<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=2">
<meta name="theme-color" content="#222">
<meta name="generator" content="Hexo 5.4.0">
  <link rel="apple-touch-icon" sizes="180x180" href="/images/Frog_32px_1177822_easyicon.net.ico">
  <link rel="icon" type="image/png" sizes="32x32" href="/images/Frog_32px_1177822_easyicon.net.ico">
  <link rel="icon" type="image/png" sizes="16x16" href="/images/Frog_16px_1177822_easyicon.net.ico">
  <link rel="mask-icon" href="/images/Frog_32px_1177822_easyicon.net.ico" color="#222">

<link rel="stylesheet" href="/css/main.css">


<link rel="stylesheet" href="/lib/font-awesome/css/all.min.css">
  <link rel="stylesheet" href="/lib/pace/pace-theme-minimal.min.css">
  <script src="/lib/pace/pace.min.js"></script>

<script id="hexo-configurations">
    var NexT = window.NexT || {};
    var CONFIG = {"hostname":"hxy1997.xyz","root":"/","scheme":"Pisces","version":"7.8.0","exturl":false,"sidebar":{"position":"left","padding":18,"offset":12,"onmobile":false},"copycode":{"enable":false,"show_result":false,"style":null},"back2top":{"enable":true,"sidebar":false,"scrollpercent":false},"bookmark":{"enable":false,"color":"#222","save":"auto"},"fancybox":false,"mediumzoom":false,"lazyload":true,"pangu":false,"comments":{"style":"tabs","active":"valine","storage":true,"lazyload":true,"nav":null,"activeClass":"valine"},"algolia":{"hits":{"per_page":10},"labels":{"input_placeholder":"输入关键字","hits_empty":"没有找到与「${query}」相关搜索","hits_stats":"${hits} 条相关记录，共耗时 ${time} ms"}},"localsearch":{"enable":true,"trigger":"auto","top_n_per_article":1,"unescape":false,"preload":false},"motion":{"enable":true,"async":false,"transition":{"post_block":"fadeIn","post_header":"slideDownIn","post_body":"slideDownIn","coll_header":"slideLeftIn","sidebar":"slideUpIn"}},"path":"search.json"};
  </script>

  <meta name="description" content="这个部分用来介绍Node相关知识，很遗憾没有做过一个完整的Node项目，掌握度不足，但Vue实战中的打包过程用了很多相关知识，赶紧补起来，这一块实战中由于脚手架的出现，已经相对遗忘了，要自己写一个配置，就需要好好掌握了">
<meta property="og:type" content="article">
<meta property="og:title" content="项目工程化与Node">
<meta property="og:url" content="https://hxy1997.xyz/2021/03/17/%E9%A1%B9%E7%9B%AE%E5%B7%A5%E7%A8%8B%E5%8C%96%E4%B8%8ENode/index.html">
<meta property="og:site_name" content="hxy的博客">
<meta property="og:description" content="这个部分用来介绍Node相关知识，很遗憾没有做过一个完整的Node项目，掌握度不足，但Vue实战中的打包过程用了很多相关知识，赶紧补起来，这一块实战中由于脚手架的出现，已经相对遗忘了，要自己写一个配置，就需要好好掌握了">
<meta property="og:locale" content="zh_CN">
<meta property="og:image" content="https://cdn.jsdelivr.net/gh/huxingyi1997/my_img/img/20210420160044.webp">
<meta property="og:image" content="https://cdn.jsdelivr.net/gh/huxingyi1997/my_img/img/20210420160054.webp">
<meta property="og:image" content="https://cdn.jsdelivr.net/gh/huxingyi1997/my_img/img/20210420160114.webp">
<meta property="og:image" content="https://cdn.jsdelivr.net/gh/huxingyi1997/my_img/img/20210420160142.webp">
<meta property="og:image" content="https://cdn.jsdelivr.net/gh/huxingyi1997/my_img/img/20210420160308.webp">
<meta property="og:image" content="https://cdn.jsdelivr.net/gh/huxingyi1997/my_img/img/20210420160322.webp">
<meta property="og:image" content="https://cdn.jsdelivr.net/gh/huxingyi1997/my_img/img/20210420160334.gif">
<meta property="og:image" content="https://cdn.jsdelivr.net/gh/huxingyi1997/my_img/img/20210420160344.gif">
<meta property="og:image" content="https://cdn.jsdelivr.net/gh/huxingyi1997/my_img/img/20210420160405.png">
<meta property="og:image" content="https://cdn.jsdelivr.net/gh/huxingyi1997/my_img/img/20210420160416.png">
<meta property="og:image" content="https://cdn.jsdelivr.net/gh/huxingyi1997/my_img/img/20210420160559.png">
<meta property="og:image" content="https://cdn.jsdelivr.net/gh/huxingyi1997/my_img/img/20210420160712.png">
<meta property="og:image" content="https://cdn.jsdelivr.net/gh/huxingyi1997/my_img/img/20210420160730.png">
<meta property="og:image" content="https://cdn.jsdelivr.net/gh/huxingyi1997/my_img/img/20210420164618.webp">
<meta property="og:image" content="https://cdn.jsdelivr.net/gh/huxingyi1997/my_img/img/20210420164700.gif">
<meta property="og:image" content="https://cdn.jsdelivr.net/gh/huxingyi1997/my_img/img/20210420164718.webp">
<meta property="og:image" content="https://cdn.jsdelivr.net/gh/huxingyi1997/my_img/img/20210420164736.webp">
<meta property="og:image" content="https://cdn.jsdelivr.net/gh/huxingyi1997/my_img/img/20210420164745.webp">
<meta property="og:image" content="https://cdn.jsdelivr.net/gh/huxingyi1997/my_img/img/20210420164837.webp">
<meta property="og:image" content="https://cdn.jsdelivr.net/gh/huxingyi1997/my_img/img/20210420164856.webp">
<meta property="og:image" content="https://cdn.jsdelivr.net/gh/huxingyi1997/my_img/img/20210420164918.webp">
<meta property="og:image" content="https://cdn.jsdelivr.net/gh/huxingyi1997/my_img/img/20210420164924.webp">
<meta property="og:image" content="https://cdn.jsdelivr.net/gh/huxingyi1997/my_img/img/20210420165014.webp">
<meta property="og:image" content="https://cdn.jsdelivr.net/gh/huxingyi1997/my_img/img/20210420165028.webp">
<meta property="og:image" content="https://cdn.jsdelivr.net/gh/huxingyi1997/my_img/img/20210420165134.webp">
<meta property="og:image" content="https://cdn.jsdelivr.net/gh/huxingyi1997/my_img/img/20210420165146.webp">
<meta property="og:image" content="https://cdn.jsdelivr.net/gh/huxingyi1997/my_img/img/20210420165325.webp">
<meta property="og:image" content="https://cdn.jsdelivr.net/gh/huxingyi1997/my_img/img/20210420165433.webp">
<meta property="og:image" content="https://cdn.jsdelivr.net/gh/huxingyi1997/my_img/img/20210420165505.webp">
<meta property="og:image" content="https://cdn.jsdelivr.net/gh/huxingyi1997/my_img/img/20210420165616.webp">
<meta property="og:image" content="https://cdn.jsdelivr.net/gh/huxingyi1997/my_img/img/20210420165702.webp">
<meta property="og:image" content="https://cdn.jsdelivr.net/gh/huxingyi1997/my_img/img/20210420165709.webp">
<meta property="og:image" content="https://cdn.jsdelivr.net/gh/huxingyi1997/my_img/img/20210420165737.webp">
<meta property="og:image" content="https://user-gold-cdn.xitu.io/2018/1/4/160bfd6bf5516b9d?imageView2/0/w/1280/h/960/format/webp/ignore-error/1">
<meta property="og:image" content="https://user-gold-cdn.xitu.io/2018/1/4/160bfd6c064d09f0?imageView2/0/w/1280/h/960/format/webp/ignore-error/1">
<meta property="og:image" content="https://user-gold-cdn.xitu.io/2018/1/4/160bfde2698a828b?imageView2/0/w/1280/h/960/format/webp/ignore-error/1">
<meta property="og:image" content="https://user-gold-cdn.xitu.io/2018/1/4/160bfde269ee60b1?imageView2/0/w/1280/h/960/format/webp/ignore-error/1">
<meta property="og:image" content="https://user-gold-cdn.xitu.io/2018/1/4/160bfde26a0b236a?imageView2/0/w/1280/h/960/format/webp/ignore-error/1">
<meta property="og:image" content="https://user-gold-cdn.xitu.io/2018/1/4/160bfde26a2f7bcd?imageView2/0/w/1280/h/960/format/webp/ignore-error/1">
<meta property="og:image" content="https://user-gold-cdn.xitu.io/2018/1/4/160bfd6bb878d28e?imageView2/0/w/1280/h/960/format/webp/ignore-error/1">
<meta property="og:image" content="https://user-gold-cdn.xitu.io/2018/1/4/160bfde277e8ea5a?imageView2/0/w/1280/h/960/format/webp/ignore-error/1">
<meta property="og:image" content="https://user-gold-cdn.xitu.io/2018/1/4/160bfde276aaf2ac?imageView2/0/w/1280/h/960/format/webp/ignore-error/1">
<meta property="og:image" content="https://user-gold-cdn.xitu.io/2018/1/4/160bfd6bb878d28e?imageView2/0/w/1280/h/960/format/webp/ignore-error/1">
<meta property="og:image" content="https://user-gold-cdn.xitu.io/2018/1/4/160bfde28789c23f?imageView2/0/w/1280/h/960/format/webp/ignore-error/1">
<meta property="og:image" content="https://user-gold-cdn.xitu.io/2018/1/4/160bfde288369a85?imageView2/0/w/1280/h/960/format/webp/ignore-error/1">
<meta property="og:image" content="https://user-gold-cdn.xitu.io/2018/1/4/160bfde28dd70e3b?imageView2/0/w/1280/h/960/format/webp/ignore-error/1">
<meta property="og:image" content="https://user-gold-cdn.xitu.io/2018/1/4/160bfde29710f82d?imageView2/0/w/1280/h/960/format/webp/ignore-error/1">
<meta property="og:image" content="https://user-gold-cdn.xitu.io/2018/1/4/160bfde2a12a6464?imageView2/0/w/1280/h/960/format/webp/ignore-error/1">
<meta property="og:image" content="https://user-gold-cdn.xitu.io/2018/1/4/160bfde2a3cd1f44?imageView2/0/w/1280/h/960/format/webp/ignore-error/1">
<meta property="og:image" content="https://user-gold-cdn.xitu.io/2018/1/4/160bfde2aa751805?imageView2/0/w/1280/h/960/format/webp/ignore-error/1">
<meta property="og:image" content="https://user-gold-cdn.xitu.io/2018/1/4/160bfde2aa34e682?imageView2/0/w/1280/h/960/format/webp/ignore-error/1">
<meta property="og:image" content="https://user-gold-cdn.xitu.io/2018/1/4/160bfde2ae93d057?imageView2/0/w/1280/h/960/format/webp/ignore-error/1">
<meta property="og:image" content="https://www.h5w3.com/wp-content/uploads/2020/06/bVbHPdq.png">
<meta property="og:image" content="https://www.h5w3.com/wp-content/uploads/2020/06/bVbHPdr.png">
<meta property="og:image" content="https://www.h5w3.com/wp-content/uploads/2020/06/bVbDNYp.jpg">
<meta property="og:image" content="https://www.h5w3.com/wp-content/uploads/2020/06/bVbHPfO.png">
<meta property="og:image" content="https://oscimg.oschina.net/oscnet/d60e4800-53b4-466b-8cac-a78d3575d237.jpg">
<meta property="og:image" content="https://oscimg.oschina.net/oscnet/f6ecc074-bf06-4d8a-b25b-836cdc32e2c6.jpg">
<meta property="og:image" content="https://oscimg.oschina.net/oscnet/c7408a95-6cfb-43c0-9f48-e49634e254df.jpg">
<meta property="og:image" content="https://oscimg.oschina.net/oscnet/948eebb7-a184-4ba4-a82b-d500587d39a9.jpg">
<meta property="og:image" content="https://oscimg.oschina.net/oscnet/2730d973-5fbc-4ac2-9169-4922b3c6e4d5.jpg">
<meta property="og:image" content="https://oscimg.oschina.net/oscnet/822c18e6-1d5d-406f-ab28-b4dad891071f.jpg">
<meta property="og:image" content="https://oscimg.oschina.net/oscnet/1fe23797-e87f-4e81-93c2-c420c959e8a0.jpg">
<meta property="og:image" content="https://oscimg.oschina.net/oscnet/3b6def74-c525-4f94-9ebb-deafb72b7e28.jpg">
<meta property="og:image" content="https://oscimg.oschina.net/oscnet/03608f83-7e7d-4de8-bfe5-3265ade3edd8.jpg">
<meta property="og:image" content="https://img.alicdn.com/tps/TB1B0DXNXXXXXXdXFXXXXXXXXXX-368-522.jpg">
<meta property="og:image" content="https://img.alicdn.com/tps/TB1GVGFNXXXXXaTapXXXXXXXXXX-4436-4244.jpg">
<meta property="og:image" content="https://img.alicdn.com/tps/TB1kvfbNXXXXXarXpXXXXXXXXXX-500-111.jpg">
<meta property="og:image" content="https://img.alicdn.com/tps/TB1UgS4NXXXXXXZXVXXXXXXXXXX-693-940.png">
<meta property="og:image" content="https://img.alicdn.com/tps/TB1WOiRNXXXXXcJaXXXXXXXXXXX-445-1228.png">
<meta property="og:image" content="https://img.alicdn.com/tps/TB1cz5.NXXXXXc7XpXXXXXXXXXX-959-807.png">
<meta property="og:image" content="https://user-gold-cdn.xitu.io/2018/12/28/167f458ac2b1e527?imageView2/0/w/1280/h/960/format/webp/ignore-error/1">
<meta property="og:image" content="https://user-gold-cdn.xitu.io/2018/12/28/167f458d6ff8424f?imageView2/0/w/1280/h/960/format/webp/ignore-error/1">
<meta property="og:image" content="https://cloud.githubusercontent.com/assets/8401872/23387388/18895ef0-fd97-11e6-9dd7-94628c322e8e.png">
<meta property="og:image" content="https://cloud.githubusercontent.com/assets/8401872/23388226/ebcea460-fd9b-11e6-8cb8-cb2bb779ae62.png">
<meta property="og:image" content="https://antcloud-cnhz02-athomeweb-01.oss-cn-hzfinance.aliyuncs.com/image/2019-08-26/1c4ee3d5-d535-4bb4-a511-2d0e1274519d.png">
<meta property="og:image" content="https://antcloud-cnhz02-athomeweb-01.oss-cn-hzfinance.aliyuncs.com/image/2019-08-26/00e4ae14-6a48-4118-a9d3-bdcbf2debadd.png">
<meta property="og:image" content="https://antcloud-cnhz02-athomeweb-01.oss-cn-hzfinance.aliyuncs.com/image/2019-08-26/86e06958-dc1b-404a-9aaa-7b7a33c7bab8.png">
<meta property="og:image" content="https://antcloud-cnhz02-athomeweb-01.oss-cn-hzfinance.aliyuncs.com/image/2019-08-26/ce38b308-f0c6-402d-a9fa-30cdb5424706.png">
<meta property="og:image" content="https://antcloud-cnhz02-athomeweb-01.oss-cn-hzfinance.aliyuncs.com/image/2019-08-26/faaf3ace-15a2-4db9-875e-92b41035d6c8.png">
<meta property="og:image" content="https://antcloud-cnhz02-athomeweb-01.oss-cn-hzfinance.aliyuncs.com/image/2019-08-26/da63a733-edca-4e57-a1cd-f9e4358c5609.png">
<meta property="og:image" content="https://antcloud-cnhz02-athomeweb-01.oss-cn-hzfinance.aliyuncs.com/image/2019-08-26/73cc3732-55af-4d60-b70a-9f2d0b206726.png">
<meta property="og:image" content="https://antcloud-cnhz02-athomeweb-01.oss-cn-hzfinance.aliyuncs.com/image/2019-08-26/c6c206b5-bf52-4494-ad0b-b443f9abb467.png">
<meta property="og:image" content="https://antcloud-cnhz02-athomeweb-01.oss-cn-hzfinance.aliyuncs.com/image/2019-08-26/74436fb6-dad1-4af6-9318-db5e217622a3.png">
<meta property="og:image" content="https://antcloud-cnhz02-athomeweb-01.oss-cn-hzfinance.aliyuncs.com/image/2019-08-26/ce4bd74e-eb49-4538-bfc0-8b639d294c24.png">
<meta property="og:image" content="https://antcloud-cnhz02-athomeweb-01.oss-cn-hzfinance.aliyuncs.com/image/2019-08-26/24ba6c51-5e32-4f65-b700-1e527ed44dc3.png">
<meta property="og:image" content="https://antcloud-cnhz02-athomeweb-01.oss-cn-hzfinance.aliyuncs.com/image/2019-08-26/cf7eb534-e522-4da7-84e5-6c314f9d52a3.png">
<meta property="og:image" content="https://antcloud-cnhz02-athomeweb-01.oss-cn-hzfinance.aliyuncs.com/image/2019-08-26/f9b794e2-24cc-40c8-bd57-7254c93d5908.png">
<meta property="og:image" content="https://antcloud-cnhz02-athomeweb-01.oss-cn-hzfinance.aliyuncs.com/image/2019-08-26/f44b35de-1d21-45dd-9998-e65cc52c0266.png">
<meta property="og:image" content="https://user-gold-cdn.xitu.io/2019/12/25/16f38d1d79c552e0?imageView2/0/w/1280/h/960/format/webp/ignore-error/1">
<meta property="og:image" content="http://www.ruanyifeng.com/blogimg/asset/2015/bg2015052001.png">
<meta property="og:image" content="https://cdn.learnku.com/uploads/images/201912/27/20604/NclgxSvw3g.png!large">
<meta property="og:image" content="https://img2018.cnblogs.com/blog/1354575/201906/1354575-20190601090434633-1515670765.png">
<meta property="og:image" content="https://img2018.cnblogs.com/blog/1354575/201906/1354575-20190601090447906-116651620.png">
<meta property="og:image" content="https://img2018.cnblogs.com/blog/1354575/201906/1354575-20190601090509233-278998873.png">
<meta property="og:image" content="https://img2018.cnblogs.com/blog/1354575/201906/1354575-20190601090521479-130930914.png">
<meta property="og:image" content="https://img2018.cnblogs.com/blog/1354575/201906/1354575-20190601090532829-2120517530.png">
<meta property="og:image" content="https://img2018.cnblogs.com/blog/1354575/201906/1354575-20190601090551857-551223998.png">
<meta property="og:image" content="http://www.barretlee.com/blogimgs/2017/06/06/node-stream-readable-flowing.png">
<meta property="og:image" content="http://www.barretlee.com/blogimgs/2017/06/06/node-stream-non-flowing.png">
<meta property="og:image" content="http://www.barretlee.com/blogimgs/2017/06/06/node-stream-writable.png">
<meta property="og:image" content="http://www.barretlee.com/blogimgs/2017/06/06/node-stream-duplex.png">
<meta property="og:image" content="http://www.barretlee.com/blogimgs/2017/06/06/node-stream-transform.png">
<meta property="og:image" content="https://images2015.cnblogs.com/blog/561179/201701/561179-20170126172845566-1089400487.gif">
<meta property="og:image" content="https://images2015.cnblogs.com/blog/561179/201701/561179-20170126170225816-1851442511.gif">
<meta property="og:image" content="https://user-gold-cdn.xitu.io/2016/11/29/7a21cc1b1975ad2128ed094f5e8effc6.png?imageView2/0/w/1280/h/960/format/webp/ignore-error/1">
<meta property="og:image" content="https://user-gold-cdn.xitu.io/2016/11/29/52bf06943ce2b7f2acfc9095bb202ece.png?imageView2/0/w/1280/h/960/format/webp/ignore-error/1">
<meta property="og:image" content="https://user-gold-cdn.xitu.io/2016/11/29/04ffe72d6460092a8b5796f6f6099734.png?imageView2/0/w/1280/h/960/format/webp/ignore-error/1">
<meta property="og:image" content="https://user-gold-cdn.xitu.io/2016/11/29/c05d3cae380bb9f107f09793c8c67923.png?imageView2/0/w/1280/h/960/format/webp/ignore-error/1">
<meta property="article:published_time" content="2021-03-17T14:14:39.000Z">
<meta property="article:modified_time" content="2021-04-20T08:58:00.816Z">
<meta property="article:author" content="hxy">
<meta property="article:tag" content="面试">
<meta property="article:tag" content="javascript">
<meta property="article:tag" content="Node">
<meta name="twitter:card" content="summary">
<meta name="twitter:image" content="https://cdn.jsdelivr.net/gh/huxingyi1997/my_img/img/20210420160044.webp">

<link rel="canonical" href="https://hxy1997.xyz/2021/03/17/%E9%A1%B9%E7%9B%AE%E5%B7%A5%E7%A8%8B%E5%8C%96%E4%B8%8ENode/">


<script id="page-configurations">
  // https://hexo.io/docs/variables.html
  CONFIG.page = {
    sidebar: "",
    isHome : false,
    isPost : true,
    lang   : 'zh-CN'
  };
</script>

  <title>项目工程化与Node | hxy的博客</title>
  






  <noscript>
  <style>
  .use-motion .brand,
  .use-motion .menu-item,
  .sidebar-inner,
  .use-motion .post-block,
  .use-motion .pagination,
  .use-motion .comments,
  .use-motion .post-header,
  .use-motion .post-body,
  .use-motion .collection-header { opacity: initial; }

  .use-motion .site-title,
  .use-motion .site-subtitle {
    opacity: initial;
    top: initial;
  }

  .use-motion .logo-line-before i { left: initial; }
  .use-motion .logo-line-after i { right: initial; }
  </style>
</noscript>

<link rel="alternate" href="/atom.xml" title="hxy的博客" type="application/atom+xml">
</head>

<body itemscope itemtype="http://schema.org/WebPage">
  <div class="container use-motion">
    <div class="headband"></div>

    <header class="header" itemscope itemtype="http://schema.org/WPHeader">
      <div class="header-inner"><div class="site-brand-container">
  <div class="site-nav-toggle">
    <div class="toggle" aria-label="切换导航栏">
      <span class="toggle-line toggle-line-first"></span>
      <span class="toggle-line toggle-line-middle"></span>
      <span class="toggle-line toggle-line-last"></span>
    </div>
  </div>

  <div class="site-meta">

    <a href="/" class="brand" rel="start">
      <span class="logo-line-before"><i></i></span>
      <h1 class="site-title">hxy的博客</h1>
      <span class="logo-line-after"><i></i></span>
    </a>
      <p class="site-subtitle" itemprop="description">Mia san Mia!</p>
  </div>

  <div class="site-nav-right">
    <div class="toggle popup-trigger">
        <i class="fa fa-search fa-fw fa-lg"></i>
    </div>
  </div>
</div>




<nav class="site-nav">
  <ul id="menu" class="main-menu menu">
        <li class="menu-item menu-item-home">

    <a href="/" rel="section"><i class="fa fa-home fa-fw"></i>首页</a>

  </li>
        <li class="menu-item menu-item-about">

    <a href="/about/" rel="section"><i class="fa fa-user fa-fw"></i>关于</a>

  </li>
        <li class="menu-item menu-item-tags">

    <a href="/tags/" rel="section"><i class="fa fa-tags fa-fw"></i>标签</a>

  </li>
        <li class="menu-item menu-item-categories">

    <a href="/categories/" rel="section"><i class="fa fa-th fa-fw"></i>分类</a>

  </li>
        <li class="menu-item menu-item-archives">

    <a href="/archives/" rel="section"><i class="fa fa-archive fa-fw"></i>归档</a>

  </li>
      <li class="menu-item menu-item-search">
        <a role="button" class="popup-trigger"><i class="fa fa-search fa-fw"></i>搜索
        </a>
      </li>
  </ul>
</nav>



  <div class="search-pop-overlay">
    <div class="popup search-popup">
        <div class="search-header">
  <span class="search-icon">
    <i class="fa fa-search"></i>
  </span>
  <div class="search-input-container">
    <input autocomplete="off" autocapitalize="off"
           placeholder="搜索..." spellcheck="false"
           type="search" class="search-input">
  </div>
  <span class="popup-btn-close">
    <i class="fa fa-times-circle"></i>
  </span>
</div>
<div id="search-result">
  <div id="no-result">
    <i class="fa fa-spinner fa-pulse fa-5x fa-fw"></i>
  </div>
</div>

    </div>
  </div>

</div>
    </header>

    
  <div class="back-to-top">
    <i class="fa fa-arrow-up"></i>
    <span>0%</span>
  </div>
  <div class="reading-progress-bar"></div>

  <a href="https://github.com/huxingyi1997" class="github-corner" title="Follow me on GitHub" aria-label="Follow me on GitHub" rel="noopener" target="_blank"><svg width="80" height="80" viewBox="0 0 250 250" aria-hidden="true"><path d="M0,0 L115,115 L130,115 L142,142 L250,250 L250,0 Z"></path><path d="M128.3,109.0 C113.8,99.7 119.0,89.6 119.0,89.6 C122.0,82.7 120.5,78.6 120.5,78.6 C119.2,72.0 123.4,76.3 123.4,76.3 C127.3,80.9 125.5,87.3 125.5,87.3 C122.9,97.6 130.6,101.9 134.4,103.2" fill="currentColor" style="transform-origin: 130px 106px;" class="octo-arm"></path><path d="M115.0,115.0 C114.9,115.1 118.7,116.5 119.8,115.4 L133.7,101.6 C136.9,99.2 139.9,98.4 142.2,98.6 C133.8,88.0 127.5,74.4 143.8,58.0 C148.5,53.4 154.0,51.2 159.7,51.0 C160.3,49.4 163.2,43.6 171.4,40.1 C171.4,40.1 176.1,42.5 178.8,56.2 C183.1,58.6 187.2,61.8 190.9,65.4 C194.5,69.0 197.7,73.2 200.1,77.6 C213.8,80.2 216.3,84.9 216.3,84.9 C212.7,93.1 206.9,96.0 205.4,96.6 C205.1,102.4 203.0,107.8 198.3,112.5 C181.9,128.9 168.3,122.5 157.7,114.1 C157.9,116.9 156.7,120.9 152.7,124.9 L141.0,136.5 C139.8,137.7 141.6,141.9 141.8,141.8 Z" fill="currentColor" class="octo-body"></path></svg></a>


    <main class="main">
      <div class="main-inner">
        <div class="content-wrap">
          

          <div class="content post posts-expand">
            

    
  
  
  <article itemscope itemtype="http://schema.org/Article" class="post-block" lang="zh-CN">
    <link itemprop="mainEntityOfPage" href="https://hxy1997.xyz/2021/03/17/%E9%A1%B9%E7%9B%AE%E5%B7%A5%E7%A8%8B%E5%8C%96%E4%B8%8ENode/">

    <span hidden itemprop="author" itemscope itemtype="http://schema.org/Person">
      <meta itemprop="image" content="/images/Robben.gif">
      <meta itemprop="name" content="hxy">
      <meta itemprop="description" content="">
    </span>

    <span hidden itemprop="publisher" itemscope itemtype="http://schema.org/Organization">
      <meta itemprop="name" content="hxy的博客">
    </span>
      <header class="post-header">
        <h1 class="post-title" itemprop="name headline">
          项目工程化与Node
        </h1>

        <div class="post-meta">
            <span class="post-meta-item">
              <span class="post-meta-item-icon">
                <i class="far fa-calendar"></i>
              </span>
              <span class="post-meta-item-text">发表于</span>

              <time title="创建时间：2021-03-17 22:14:39" itemprop="dateCreated datePublished" datetime="2021-03-17T22:14:39+08:00">2021-03-17</time>
            </span>
              <span class="post-meta-item">
                <span class="post-meta-item-icon">
                  <i class="far fa-calendar-check"></i>
                </span>
                <span class="post-meta-item-text">更新于</span>
                <time title="修改时间：2021-04-20 16:58:00" itemprop="dateModified" datetime="2021-04-20T16:58:00+08:00">2021-04-20</time>
              </span>
            <span class="post-meta-item">
              <span class="post-meta-item-icon">
                <i class="far fa-folder"></i>
              </span>
              <span class="post-meta-item-text">分类于</span>
                <span itemprop="about" itemscope itemtype="http://schema.org/Thing">
                  <a href="/categories/web%E5%89%8D%E7%AB%AF/" itemprop="url" rel="index"><span itemprop="name">web前端</span></a>
                </span>
            </span>

          
            <span class="post-meta-item" title="热度" id="busuanzi_container_page_pv" style="display: none;">
              <span class="post-meta-item-icon">
                <i class="fa fa-eye"></i>
              </span>
              <span class="post-meta-item-text">热度：</span>
              <span id="busuanzi_value_page_pv"></span>
            </span>
  
  <span class="post-meta-item">
    
      <span class="post-meta-item-icon">
        <i class="far fa-comment"></i>
      </span>
      <span class="post-meta-item-text">Valine：</span>
    
    <a title="valine" href="/2021/03/17/%E9%A1%B9%E7%9B%AE%E5%B7%A5%E7%A8%8B%E5%8C%96%E4%B8%8ENode/#valine-comments" itemprop="discussionUrl">
      <span class="post-comments-count valine-comment-count" data-xid="/2021/03/17/%E9%A1%B9%E7%9B%AE%E5%B7%A5%E7%A8%8B%E5%8C%96%E4%B8%8ENode/" itemprop="commentCount"></span>
    </a>
  </span>
  
  

        </div>
      </header>

    
    
    
    <div class="post-body" itemprop="articleBody">

      
        <p>这个部分用来介绍Node相关知识，很遗憾没有做过一个完整的Node项目，掌握度不足，但Vue实战中的打包过程用了很多相关知识，赶紧补起来，这一块实战中由于脚手架的出现，已经相对遗忘了，要自己写一个配置，就需要好好掌握了</p>
<span id="more"></span>

<h1 id="1-浏览器与Node的事件循环-Event-Loop-有何区别"><a href="#1-浏览器与Node的事件循环-Event-Loop-有何区别" class="headerlink" title="1. 浏览器与Node的事件循环(Event Loop)有何区别?"></a>1. 浏览器与Node的事件循环(Event Loop)有何区别?</h1><h2 id="1-1-线程与进程"><a href="#1-1-线程与进程" class="headerlink" title="1.1 线程与进程"></a>1.1 线程与进程</h2><h3 id="1-概念"><a href="#1-概念" class="headerlink" title="1.概念"></a>1.概念</h3><p>我们经常说JS 是单线程执行的，指的是一个进程里只有一个主线程，那到底什么是线程？什么是进程？</p>
<p>官方的说法是：<strong>进程是 CPU资源分配的最小单位；线程是 CPU调度的最小单位</strong>。这两句话并不好理解，我们先来看张图：</p>
<p><img data-src="https://cdn.jsdelivr.net/gh/huxingyi1997/my_img/img/20210420160044.webp" alt="img"></p>
<ul>
<li>进程好比图中的工厂，有单独的专属自己的工厂资源。</li>
<li>线程好比图中的工人，多个工人在一个工厂中协作工作，工厂与工人是 1:n的关系。也就是说<strong>一个进程由一个或多个线程组成，线程是一个进程中代码的不同执行路线</strong>；</li>
<li>工厂的空间是工人们共享的，这象征<strong>一个进程的内存空间是共享的，每个线程都可用这些共享内存</strong>。</li>
<li>多个工厂之间独立存在。</li>
</ul>
<h3 id="2-多进程与多线程"><a href="#2-多进程与多线程" class="headerlink" title="2.多进程与多线程"></a>2.多进程与多线程</h3><ul>
<li>多进程：在同一个时间里，同一个计算机系统中如果允许两个或两个以上的进程处于运行状态。多进程带来的好处是明显的，比如你可以听歌的同时，打开编辑器敲代码，编辑器和听歌软件的进程之间丝毫不会相互干扰。</li>
<li>多线程：程序中包含多个执行流，即在一个程序中可以同时运行多个不同的线程来执行不同的任务，也就是说允许单个程序创建多个并行执行的线程来完成各自的任务。</li>
</ul>
<p>以Chrome浏览器中为例，当你打开一个 Tab 页时，其实就是创建了一个进程，一个进程中可以有多个线程（下文会详细介绍），比如渲染线程、JS 引擎线程、HTTP 请求线程等等。当你发起一个请求时，其实就是创建了一个线程，当请求结束后，该线程可能就会被销毁。</p>
<h2 id="1-2-浏览器内核"><a href="#1-2-浏览器内核" class="headerlink" title="1.2 浏览器内核"></a>1.2 浏览器内核</h2><p>简单来说浏览器内核是通过取得页面内容、整理信息（应用CSS）、计算和组合最终输出可视化的图像结果，通常也被称为渲染引擎。</p>
<p>浏览器内核是多线程，在内核控制下各线程相互配合以保持同步，一个浏览器通常由以下常驻线程组成：</p>
<ul>
<li>GUI 渲染线程</li>
<li>JavaScript引擎线程</li>
<li>定时触发器线程</li>
<li>事件触发线程</li>
<li>异步http请求线程</li>
</ul>
<h3 id="1-2-1-GUI渲染线程"><a href="#1-2-1-GUI渲染线程" class="headerlink" title="1.2.1 GUI渲染线程"></a>1.2.1 GUI渲染线程</h3><ul>
<li>主要负责页面的渲染，解析HTML、CSS，构建DOM树，布局和绘制等。</li>
<li>当界面需要重绘或者由于某种操作引发回流时，将执行该线程。</li>
<li>该线程与JS引擎线程互斥，当执行JS引擎线程时，GUI渲染会被挂起，当任务队列空闲时，主线程才会去执行GUI渲染。</li>
</ul>
<h3 id="1-2-2-JS引擎线程"><a href="#1-2-2-JS引擎线程" class="headerlink" title="1.2.2 JS引擎线程"></a>1.2.2 JS引擎线程</h3><ul>
<li>该线程当然是主要负责处理 JavaScript脚本，执行代码。</li>
<li>也是主要负责执行准备好待执行的事件，即定时器计数结束，或者异步请求成功并正确返回时，将依次进入任务队列，等待 JS引擎线程的执行。</li>
<li>当然，该线程与 GUI渲染线程互斥，当 JS引擎线程执行 JavaScript脚本时间过长，将导致页面渲染的阻塞。</li>
</ul>
<h3 id="1-2-3-定时器触发线程"><a href="#1-2-3-定时器触发线程" class="headerlink" title="1.2.3 定时器触发线程"></a>1.2.3 定时器触发线程</h3><ul>
<li>负责执行异步定时器一类的函数的线程，如： setTimeout，setInterval。</li>
<li>主线程依次执行代码时，遇到定时器，会将定时器交给该线程处理，当计数完毕后，事件触发线程会将计数完毕后的事件加入到任务队列的尾部，等待JS引擎线程执行。</li>
</ul>
<h3 id="1-2-4事件触发线程"><a href="#1-2-4事件触发线程" class="headerlink" title="1.2.4事件触发线程"></a>1.2.4事件触发线程</h3><ul>
<li>主要负责将准备好的事件交给 JS引擎线程执行。</li>
</ul>
<p>比如 setTimeout定时器计数结束， ajax等异步请求成功并触发回调函数，或者用户触发点击事件时，该线程会将整装待发的事件依次加入到任务队列的队尾，等待 JS引擎线程的执行。</p>
<h3 id="1-2-5-异步http请求线程"><a href="#1-2-5-异步http请求线程" class="headerlink" title="1.2.5 异步http请求线程"></a>1.2.5 异步http请求线程</h3><ul>
<li>负责执行异步请求一类的函数的线程，如： Promise，axios，ajax等。</li>
<li>主线程依次执行代码时，遇到异步请求，会将函数交给该线程处理，当监听到状态码变更，如果有回调函数，事件触发线程会将回调函数加入到任务队列的尾部，等待JS引擎线程执行。</li>
</ul>
<h2 id="1-3-浏览器中的-Event-Loop"><a href="#1-3-浏览器中的-Event-Loop" class="headerlink" title="1.3 浏览器中的 Event Loop"></a>1.3 浏览器中的 Event Loop</h2><h3 id="1-3-1-Micro-Task-与-Macro-Task"><a href="#1-3-1-Micro-Task-与-Macro-Task" class="headerlink" title="1.3.1 Micro-Task 与 Macro-Task"></a>1.3.1 Micro-Task 与 Macro-Task</h3><p>浏览器端事件循环中的异步队列有两种：macro（宏任务）队列和 micro（微任务）队列。<strong>宏任务队列可以有多个，微任务队列只有一个</strong>。</p>
<ul>
<li>常见的 macro-task 比如：setTimeout、setInterval、script（整体代码）、 I/O 操作、UI 渲染等。</li>
<li>常见的 micro-task 比如: new Promise().then(回调)、MutationObserver(html5新特性) 等。</li>
</ul>
<h3 id="1-3-2-Event-Loop-过程解析"><a href="#1-3-2-Event-Loop-过程解析" class="headerlink" title="1.3.2 Event Loop 过程解析"></a>1.3.2 Event Loop 过程解析</h3><p>一个完整的 Event Loop 过程，可以概括为以下阶段：</p>
<p><img data-src="https://cdn.jsdelivr.net/gh/huxingyi1997/my_img/img/20210420160054.webp" alt="img"></p>
<ul>
<li>一开始执行栈空,我们可以把<strong>执行栈认为是一个存储函数调用的栈结构，遵循先进后出的原则</strong>。micro 队列空，macro 队列里有且只有一个 script 脚本（整体代码）。</li>
<li>全局上下文（script 标签）被推入执行栈，同步代码执行。在执行的过程中，会判断是同步任务还是异步任务，通过对一些接口的调用，可以产生新的 macro-task 与 micro-task，它们会分别被推入各自的任务队列里。同步代码执行完了，script 脚本会被移出 macro 队列，这个过程本质上是队列的 macro-task 的执行和出队的过程。</li>
<li>上一步我们出队的是一个 macro-task，这一步我们处理的是 micro-task。但需要注意的是：当 macro-task 出队时，任务是<strong>一个一个</strong>执行的；而 micro-task 出队时，任务是<strong>一队一队</strong>执行的。因此，我们处理 micro 队列这一步，会逐个执行队列中的任务并把它出队，直到队列被清空。</li>
<li><strong>执行渲染操作，更新界面</strong></li>
<li>检查是否存在 Web worker 任务，如果有，则对其进行处理</li>
<li>上述过程循环往复，直到两个队列都清空</li>
</ul>
<p>我们总结一下，每一次循环都是一个这样的过程：</p>
<p><img data-src="https://cdn.jsdelivr.net/gh/huxingyi1997/my_img/img/20210420160114.webp" alt="img"></p>
<p><strong>当某个宏任务执行完后,会查看是否有微任务队列。如果有，先执行微任务队列中的所有任务，如果没有，会读取宏任务队列中排在最前的任务，执行宏任务的过程中，遇到微任务，依次加入微任务队列。栈空后，再次读取微任务队列里的任务，依次类推。</strong></p>
<p>接下来我们看道例子来介绍上面流程：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line">Promise.resolve().then(()&#x3D;&gt;&#123;</span><br><span class="line">  console.log(&#39;Promise1&#39;)  </span><br><span class="line">  setTimeout(()&#x3D;&gt;&#123;</span><br><span class="line">    console.log(&#39;setTimeout2&#39;)</span><br><span class="line">  &#125;,0)</span><br><span class="line">&#125;)</span><br><span class="line">setTimeout(()&#x3D;&gt;&#123;</span><br><span class="line">  console.log(&#39;setTimeout1&#39;)</span><br><span class="line">  Promise.resolve().then(()&#x3D;&gt;&#123;</span><br><span class="line">    console.log(&#39;Promise2&#39;)    </span><br><span class="line">  &#125;)</span><br><span class="line">&#125;,0)</span><br></pre></td></tr></table></figure>

<p>最后输出结果是Promise1，setTimeout1，Promise2，setTimeout2</p>
<ul>
<li>一开始执行栈的同步任务（这属于宏任务）执行完毕，会去查看是否有微任务队列，上题中存在(有且只有一个)，然后执行微任务队列中的所有任务输出Promise1，同时会生成一个宏任务 setTimeout2</li>
<li>然后去查看宏任务队列，宏任务 setTimeout1 在 setTimeout2 之前，先执行宏任务 setTimeout1，输出 setTimeout1</li>
<li>在执行宏任务setTimeout1时会生成微任务Promise2 ，放入微任务队列中，接着先去清空微任务队列中的所有任务，输出 Promise2</li>
<li>清空完微任务队列中的所有任务后，就又会去宏任务队列取一个，这回执行的是 setTimeout2</li>
</ul>
<h2 id="1-4-Node-中的-Event-Loop"><a href="#1-4-Node-中的-Event-Loop" class="headerlink" title="1.4 Node 中的 Event Loop"></a>1.4 Node 中的 Event Loop</h2><h3 id="1-4-1-Node简介"><a href="#1-4-1-Node简介" class="headerlink" title="1.4.1 Node简介"></a>1.4.1 Node简介</h3><p>Node 中的 Event Loop 和浏览器中的是完全不相同的东西。Node.js采用V8作为js的解析引擎，而I/O处理方面使用了自己设计的libuv，libuv是一个基于事件驱动的跨平台抽象层，封装了不同操作系统一些底层特性，对外提供统一的API，事件循环机制也是它里面的实现（下文会详细介绍）。</p>
<p><img data-src="https://cdn.jsdelivr.net/gh/huxingyi1997/my_img/img/20210420160142.webp" alt="img"></p>
<p>Node.js的运行机制如下:</p>
<ul>
<li>V8引擎解析JavaScript脚本。</li>
<li>解析后的代码，调用Node API。</li>
<li>libuv库负责Node API的执行。它将不同的任务分配给不同的线程，形成一个Event Loop（事件循环），以异步的方式将任务的执行结果返回给V8引擎。</li>
<li>V8引擎再将结果返回给用户。</li>
</ul>
<h3 id="1-4-2-六个阶段"><a href="#1-4-2-六个阶段" class="headerlink" title="1.4.2 六个阶段"></a>1.4.2 六个阶段</h3><p>其中libuv引擎中的事件循环分为 6 个阶段，它们会按照顺序反复运行。每当进入某一个阶段的时候，都会从对应的回调队列中取出函数去执行。当队列为空或者执行的回调函数数量到达系统设定的阈值，就会进入下一阶段。</p>
<p><img data-src="https://cdn.jsdelivr.net/gh/huxingyi1997/my_img/img/20210420160308.webp" alt="img"></p>
<p>从上图中，大致看出node中的事件循环的顺序：</p>
<p>外部输入数据–&gt;轮询阶段(poll)–&gt;检查阶段(check)–&gt;关闭事件回调阶段(close callback)–&gt;定时器检测阶段(timer)–&gt;I/O事件回调阶段(I/O callbacks)–&gt;闲置阶段(idle, prepare)–&gt;轮询阶段（按照该顺序反复运行）…</p>
<ul>
<li>timers 阶段：这个阶段执行timer（setTimeout、setInterval）的回调</li>
<li>I/O callbacks 阶段：处理一些上一轮循环中的少数未执行的 I/O 回调</li>
<li>idle, prepare 阶段：仅node内部使用</li>
<li>poll 阶段：获取新的I/O事件, 适当的条件下node将阻塞在这里</li>
<li>check 阶段：执行 setImmediate() 的回调</li>
<li>close callbacks 阶段：执行 socket 的 close 事件回调</li>
</ul>
<p>注意：<strong>上面六个阶段都不包括 process.nextTick()</strong>(下文会介绍)</p>
<p>接下去我们详细介绍<code>timers</code>、<code>poll</code>、<code>check</code>这3个阶段，因为日常开发中的绝大部分异步任务都是在这3个阶段处理的。</p>
<h4 id="1-timer"><a href="#1-timer" class="headerlink" title="(1) timer"></a>(1) timer</h4><p>timers 阶段会执行 setTimeout 和 setInterval 回调，并且是由 poll 阶段控制的。 同样，<strong>在 Node 中定时器指定的时间也不是准确时间，只能是尽快执行</strong>。</p>
<h4 id="2-poll"><a href="#2-poll" class="headerlink" title="(2) poll"></a>(2) poll</h4><p>poll 是一个至关重要的阶段，这一阶段中，系统会做两件事情</p>
<p>1.回到 timer 阶段执行回调</p>
<p>2.执行 I/O 回调</p>
<p>并且在进入该阶段时如果没有设定了 timer 的话，会发生以下两件事情</p>
<ul>
<li>如果 poll 队列不为空，会遍历回调队列并同步执行，直到队列为空或者达到系统限制</li>
<li>如果 poll 队列为空时，会有两件事发生<ul>
<li>如果有 setImmediate 回调需要执行，poll 阶段会停止并且进入到 check 阶段执行回调</li>
<li>如果没有 setImmediate 回调需要执行，会等待回调被加入到队列中并立即执行回调，这里同样会有个超时时间设置防止一直等待下去</li>
</ul>
</li>
</ul>
<p>当然设定了 timer 的话且 poll 队列为空，则会判断是否有 timer 超时，如果有的话会回到 timer 阶段执行回调。</p>
<h4 id="3-check阶段"><a href="#3-check阶段" class="headerlink" title="(3) check阶段"></a>(3) check阶段</h4><p>setImmediate()的回调会被加入check队列中，从event loop的阶段图可以知道，check阶段的执行顺序在poll阶段之后。 我们先来看个例子:</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line">console.log(&#39;start&#39;)</span><br><span class="line">setTimeout(() &#x3D;&gt; &#123;</span><br><span class="line">  console.log(&#39;timer1&#39;)</span><br><span class="line">  Promise.resolve().then(function() &#123;</span><br><span class="line">    console.log(&#39;promise1&#39;)</span><br><span class="line">  &#125;)</span><br><span class="line">&#125;, 0)</span><br><span class="line">setTimeout(() &#x3D;&gt; &#123;</span><br><span class="line">  console.log(&#39;timer2&#39;)</span><br><span class="line">  Promise.resolve().then(function() &#123;</span><br><span class="line">    console.log(&#39;promise2&#39;)</span><br><span class="line">  &#125;)</span><br><span class="line">&#125;, 0)</span><br><span class="line">Promise.resolve().then(function() &#123;</span><br><span class="line">  console.log(&#39;promise3&#39;)</span><br><span class="line">&#125;)</span><br><span class="line">console.log(&#39;end&#39;)</span><br><span class="line">&#x2F;&#x2F;start&#x3D;&gt;end&#x3D;&gt;promise3&#x3D;&gt;timer1&#x3D;&gt;timer2&#x3D;&gt;promise1&#x3D;&gt;promise2</span><br></pre></td></tr></table></figure>

<ul>
<li>一开始执行栈的同步任务（这属于宏任务）执行完毕后（依次打印出start end，并将2个timer依次放入timer队列）,会先去执行微任务（<strong>这点跟浏览器端的一样</strong>），所以打印出promise3</li>
<li>然后进入timers阶段，执行timer1的回调函数，打印timer1，并将promise.then回调放入microtask队列，同样的步骤执行timer2，打印timer2；这点跟浏览器端相差比较大，<strong>timers阶段有几个setTimeout/setInterval都会依次执行</strong>，并不像浏览器端，每执行一个宏任务后就去执行一个微任务（关于Node与浏览器的 Event Loop 差异，下文还会详细介绍）。</li>
</ul>
<h3 id="1-4-3-Micro-Task-与-Macro-Task"><a href="#1-4-3-Micro-Task-与-Macro-Task" class="headerlink" title="1.4.3 Micro-Task 与 Macro-Task"></a>1.4.3 Micro-Task 与 Macro-Task</h3><p>Node端事件循环中的异步队列也是这两种：macro（宏任务）队列和 micro（微任务）队列。</p>
<ul>
<li>常见的 macro-task 比如：setTimeout、setInterval、 setImmediate、script（整体代码）、 I/O 操作等。</li>
<li>常见的 micro-task 比如: process.nextTick、new Promise().then(回调)等。</li>
</ul>
<h3 id="1-4-4-注意点"><a href="#1-4-4-注意点" class="headerlink" title="1.4.4 注意点"></a>1.4.4 注意点</h3><h4 id="1-setTimeout-和-setImmediate"><a href="#1-setTimeout-和-setImmediate" class="headerlink" title="(1) setTimeout 和 setImmediate"></a>(1) setTimeout 和 setImmediate</h4><p>二者非常相似，区别主要在于调用时机不同。</p>
<ul>
<li>setImmediate 设计在poll阶段完成时执行，即check阶段；</li>
<li>setTimeout 设计在poll阶段为空闲时，且设定时间到达后执行，但它在timer阶段执行</li>
</ul>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">setTimeout(function timeout () &#123;</span><br><span class="line">  console.log(&#39;timeout&#39;);</span><br><span class="line">&#125;,0);</span><br><span class="line">setImmediate(function immediate () &#123;</span><br><span class="line">  console.log(&#39;immediate&#39;);</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure>

<ul>
<li>对于以上代码来说，setTimeout 可能执行在前，也可能执行在后。</li>
<li>首先 setTimeout(fn, 0) === setTimeout(fn, 1)，这是由源码决定的 进入事件循环也是需要成本的，如果在准备时候花费了大于 1ms 的时间，那么在 timer 阶段就会直接执行 setTimeout 回调</li>
<li>如果准备时间花费小于 1ms，那么就是 setImmediate 回调先执行了</li>
</ul>
<p>但当二者在异步i/o callback内部调用时，总是先执行setImmediate，再执行setTimeout</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">const fs &#x3D; require(&#39;fs&#39;)</span><br><span class="line">fs.readFile(__filename, () &#x3D;&gt; &#123;</span><br><span class="line">    setTimeout(() &#x3D;&gt; &#123;</span><br><span class="line">        console.log(&#39;timeout&#39;);</span><br><span class="line">    &#125;, 0)</span><br><span class="line">    setImmediate(() &#x3D;&gt; &#123;</span><br><span class="line">        console.log(&#39;immediate&#39;)</span><br><span class="line">    &#125;)</span><br><span class="line">&#125;)</span><br><span class="line">&#x2F;&#x2F; immediate</span><br><span class="line">&#x2F;&#x2F; timeout</span><br></pre></td></tr></table></figure>

<p>在上述代码中，setImmediate 永远先执行。因为两个代码写在 IO 回调中，IO 回调是在 poll 阶段执行，当回调执行完毕后队列为空，发现存在 setImmediate 回调，所以就直接跳转到 check 阶段去执行回调了。</p>
<h4 id="2-process-nextTick"><a href="#2-process-nextTick" class="headerlink" title="(2) process.nextTick"></a>(2) process.nextTick</h4><p>这个函数其实是独立于 Event Loop 之外的，它有一个自己的队列，当每个阶段完成后，如果存在 nextTick 队列，就会清空队列中的所有回调函数，并且优先于其他 microtask 执行。</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line">setTimeout(() &#x3D;&gt; &#123;</span><br><span class="line"> console.log(&#39;timer1&#39;)</span><br><span class="line"> Promise.resolve().then(function() &#123;</span><br><span class="line">   console.log(&#39;promise1&#39;)</span><br><span class="line"> &#125;)</span><br><span class="line">&#125;, 0)</span><br><span class="line">process.nextTick(() &#x3D;&gt; &#123;</span><br><span class="line"> console.log(&#39;nextTick&#39;)</span><br><span class="line"> process.nextTick(() &#x3D;&gt; &#123;</span><br><span class="line">   console.log(&#39;nextTick&#39;)</span><br><span class="line">   process.nextTick(() &#x3D;&gt; &#123;</span><br><span class="line">     console.log(&#39;nextTick&#39;)</span><br><span class="line">     process.nextTick(() &#x3D;&gt; &#123;</span><br><span class="line">       console.log(&#39;nextTick&#39;)</span><br><span class="line">     &#125;)</span><br><span class="line">   &#125;)</span><br><span class="line"> &#125;)</span><br><span class="line">&#125;)</span><br><span class="line">&#x2F;&#x2F; nextTick&#x3D;&gt;nextTick&#x3D;&gt;nextTick&#x3D;&gt;nextTick&#x3D;&gt;timer1&#x3D;&gt;promise1</span><br></pre></td></tr></table></figure>

<h2 id="1-5-Node与浏览器的-Event-Loop-差异"><a href="#1-5-Node与浏览器的-Event-Loop-差异" class="headerlink" title="1.5 Node与浏览器的 Event Loop 差异"></a>1.5 Node与浏览器的 Event Loop 差异</h2><p><strong>浏览器环境下，microtask的任务队列是每个macrotask执行完之后执行。而在Node.js中，microtask会在事件循环的各个阶段之间执行，也就是一个阶段执行完毕，就会去执行microtask队列的任务</strong>。</p>
<p><img data-src="https://cdn.jsdelivr.net/gh/huxingyi1997/my_img/img/20210420160322.webp" alt="img"></p>
<p>接下我们通过一个例子来说明两者区别：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line">setTimeout(()&#x3D;&gt;&#123;</span><br><span class="line">    console.log(&#39;timer1&#39;)</span><br><span class="line">    Promise.resolve().then(function() &#123;</span><br><span class="line">        console.log(&#39;promise1&#39;)</span><br><span class="line">    &#125;)</span><br><span class="line">&#125;, 0)</span><br><span class="line">setTimeout(()&#x3D;&gt;&#123;</span><br><span class="line">    console.log(&#39;timer2&#39;)</span><br><span class="line">    Promise.resolve().then(function() &#123;</span><br><span class="line">        console.log(&#39;promise2&#39;)</span><br><span class="line">    &#125;)</span><br><span class="line">&#125;, 0)</span><br></pre></td></tr></table></figure>

<p>浏览器端运行结果：<code>timer1=&gt;promise1=&gt;timer2=&gt;promise2</code></p>
<p>浏览器端的处理过程如下：</p>
<p><img data-src="https://cdn.jsdelivr.net/gh/huxingyi1997/my_img/img/20210420160334.gif" alt="img"></p>
<p>Node端运行结果分两种情况：</p>
<ul>
<li>如果是node11版本一旦执行一个阶段里的一个宏任务(setTimeout,setInterval和setImmediate)就立刻执行微任务队列，这就跟浏览器端运行一致，最后的结果为<code>timer1=&gt;promise1=&gt;timer2=&gt;promise2</code></li>
<li>如果是node10及其之前版本：要看第一个定时器执行完，第二个定时器是否在完成队列中。<ul>
<li>如果是第二个定时器还未在完成队列中，最后的结果为<code>timer1=&gt;promise1=&gt;timer2=&gt;promise2</code></li>
<li>如果是第二个定时器已经在完成队列中，则最后的结果为<code>timer1=&gt;timer2=&gt;promise1=&gt;promise2</code>(下文过程解释基于这种情况下)</li>
</ul>
</li>
</ul>
<p>1.全局脚本（main()）执行，将2个timer依次放入timer队列，main()执行完毕，调用栈空闲，任务队列开始执行；</p>
<p>2.首先进入timers阶段，执行timer1的回调函数，打印timer1，并将promise1.then回调放入microtask队列，同样的步骤执行timer2，打印timer2；</p>
<p>3.至此，timer阶段执行结束，event loop进入下一个阶段之前，执行microtask队列的所有任务，依次打印promise1、promise2</p>
<p>Node端的处理过程如下：</p>
<p><img data-src="https://cdn.jsdelivr.net/gh/huxingyi1997/my_img/img/20210420160344.gif" alt="img"></p>
<h2 id="1-6-总结"><a href="#1-6-总结" class="headerlink" title="1.6 总结"></a>1.6 总结</h2><p>浏览器和Node 环境下，microtask 任务队列的执行时机不同</p>
<ul>
<li>Node端，microtask 在事件循环的各个阶段之间执行</li>
<li>浏览器端，microtask 在事件循环的 macrotask 执行完之后执行</li>
</ul>
<h2 id="后记"><a href="#后记" class="headerlink" title="后记"></a>后记</h2><p>文章于2019.1.16晚，对最后一个例子在node运行结果，重新修改！再次特别感谢<a target="_blank" rel="noopener" href="https://juejin.im/user/1521379822801224">zy445566</a>的精彩点评，<strong>由于node版本更新到11，Event Loop运行原理发生了变化，一旦执行一个阶段里的一个宏任务(setTimeout,setInterval和setImmediate)就立刻执行微任务队列，这点就跟浏览器端一致</strong>。</p>
<h2 id="参考文章"><a href="#参考文章" class="headerlink" title="参考文章"></a>参考文章</h2><ul>
<li><a target="_blank" rel="noopener" href="https://imweb.io/topic/58e3bfa845e5c13468f567d5">浏览器进程？线程？傻傻分不清楚！</a></li>
<li><a target="_blank" rel="noopener" href="https://mp.weixin.qq.com/s?__biz=MzAxODE2MjM1MA==&mid=2651555331&idx=1&sn=5a063db73329a9d8ea5c38d8eeb50741&chksm=802551c2b752d8d4020514337f9987cc6611ba878525c45507cbb9ceb52b92e060f50e76c48d&mpshare=1&scene=1&srcid=1115P2jpgUGueMlTaRe9UFYu#rd">事件循环机制的那些事</a></li>
<li><a target="_blank" rel="noopener" href="https://juejin.im/book/6844733750048210957">前端性能优化原理与实践</a></li>
<li><a target="_blank" rel="noopener" href="https://juejin.im/book/6844733763675488269/section/6844733763763568654#heading-3">前端面试之道</a></li>
<li><a target="_blank" rel="noopener" href="http://lynnelv.github.io/js-event-loop-nodejs">深入理解js事件循环机制（Node.js篇）</a></li>
<li><a target="_blank" rel="noopener" href="https://zhuanlan.zhihu.com/p/33058983">详解JavaScript中的Event Loop（事件循环）机制</a></li>
<li><a target="_blank" rel="noopener" href="https://nodejs.org/en/docs/guides/event-loop-timers-and-nexttick/">event-loop-timers-and-nexttick</a></li>
<li><a target="_blank" rel="noopener" href="https://github.com/nodejs/node/pull/22842">timers: run nextTicks after each immediate and timer</a></li>
</ul>
<h1 id="2-前端模块化"><a href="#2-前端模块化" class="headerlink" title="2.前端模块化"></a>2.<a target="_blank" rel="noopener" href="https://segmentfault.com/a/1190000017466120">前端模块化</a></h1><h2 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h2><p>在JavaScript发展初期就是为了实现简单的页面交互逻辑，寥寥数语即可；如今CPU、浏览器性能得到了极大的提升，很多页面逻辑迁移到了客户端（表单验证等），随着web2.0时代的到来，Ajax技术得到广泛应用，jQuery等前端库层出不穷，前端代码日益膨胀，此时在JS方面就会考虑使用模块化规范去管理。<br>本文内容主要有理解模块化，为什么要模块化，模块化的优缺点以及模块化规范,并且介绍下开发中最流行的CommonJS, AMD, ES6、CMD规范。本文试图站在小白的角度，用通俗易懂的笔调介绍这些枯燥无味的概念，希望诸君阅读后，对模块化编程有个全新的认识和理解！</p>
<p><strong>建议下载本文源代码，自己动手敲一遍，请猛戳<a target="_blank" rel="noopener" href="https://github.com/ljianshu/Blog">GitHub个人博客</a></strong></p>
<p><img data-src="https://cdn.jsdelivr.net/gh/huxingyi1997/my_img/img/20210420160405.png" alt="模块化规范"></p>
<h2 id="2-1-模块化的理解"><a href="#2-1-模块化的理解" class="headerlink" title="2.1 模块化的理解"></a>2.1 模块化的理解</h2><h3 id="2-1-1-什么是模块"><a href="#2-1-1-什么是模块" class="headerlink" title="2.1.1 什么是模块?"></a>2.1.1 什么是模块?</h3><ul>
<li>将一个复杂的程序依据一定的规则(规范)封装成几个块(文件), 并进行组合在一起</li>
<li>块的内部数据与实现是私有的, 只是向外部暴露一些接口(方法)与外部其它模块通信</li>
</ul>
<h3 id="2-2-2-模块化的进化过程"><a href="#2-2-2-模块化的进化过程" class="headerlink" title="2.2.2 模块化的进化过程"></a>2.2.2 模块化的进化过程</h3><ul>
<li><strong>全局function模式 : 将不同的功能封装成不同的全局函数</strong><ul>
<li>编码: 将不同的功能封装成不同的全局函数</li>
<li>问题: 污染全局命名空间, 容易引起命名冲突或数据不安全，而且模块成员之间看不出直接关系</li>
</ul>
</li>
</ul>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">function m1()&#123;</span><br><span class="line">  &#x2F;&#x2F;...</span><br><span class="line">&#125;</span><br><span class="line">function m2()&#123;</span><br><span class="line">  &#x2F;&#x2F;...</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<ul>
<li><strong>namespace模式 : 简单对象封装</strong><ul>
<li>作用: 减少了全局变量，解决命名冲突</li>
<li>问题: 数据不安全(外部可以直接修改模块内部的数据)</li>
</ul>
</li>
</ul>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">let myModule &#x3D; &#123;</span><br><span class="line">  data: &#39;www.baidu.com&#39;,</span><br><span class="line">  foo() &#123;</span><br><span class="line">    console.log(&#96;foo() $&#123;this.data&#125;&#96;)</span><br><span class="line">  &#125;,</span><br><span class="line">  bar() &#123;</span><br><span class="line">    console.log(&#96;bar() $&#123;this.data&#125;&#96;)</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br><span class="line">myModule.data &#x3D; &#39;other data&#39; &#x2F;&#x2F;能直接修改模块内部的数据</span><br><span class="line">myModule.foo() &#x2F;&#x2F; foo() other data</span><br></pre></td></tr></table></figure>

<p>这样的写法会暴露所有模块成员，内部状态可以被外部改写。</p>
<ul>
<li><strong>IIFE模式：匿名函数自调用(闭包)</strong><ul>
<li>作用: 数据是私有的, 外部只能通过暴露的方法操作</li>
<li>编码: 将数据和行为封装到一个函数内部, 通过给window添加属性来向外暴露接口</li>
<li>问题: 如果当前这个模块依赖另一个模块怎么办?</li>
</ul>
</li>
</ul>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br></pre></td><td class="code"><pre><span class="line">&#x2F;&#x2F; index.html文件</span><br><span class="line">&lt;script type&#x3D;&quot;text&#x2F;javascript&quot; src&#x3D;&quot;module.js&quot;&gt;&lt;&#x2F;script&gt;</span><br><span class="line">&lt;script type&#x3D;&quot;text&#x2F;javascript&quot;&gt;</span><br><span class="line">    myModule.foo()</span><br><span class="line">    myModule.bar()</span><br><span class="line">    console.log(myModule.data) &#x2F;&#x2F;undefined 不能访问模块内部数据</span><br><span class="line">    myModule.data &#x3D; &#39;xxxx&#39; &#x2F;&#x2F;不是修改的模块内部的data</span><br><span class="line">    myModule.foo() &#x2F;&#x2F;没有改变</span><br><span class="line">&lt;&#x2F;script&gt;</span><br><span class="line">&#x2F;&#x2F; module.js文件</span><br><span class="line">(function(window) &#123;</span><br><span class="line">  let data &#x3D; &#39;www.baidu.com&#39;</span><br><span class="line">  &#x2F;&#x2F;操作数据的函数</span><br><span class="line">  function foo() &#123;</span><br><span class="line">    &#x2F;&#x2F;用于暴露有函数</span><br><span class="line">    console.log(&#96;foo() $&#123;data&#125;&#96;)</span><br><span class="line">  &#125;</span><br><span class="line">  function bar() &#123;</span><br><span class="line">    &#x2F;&#x2F;用于暴露有函数</span><br><span class="line">    console.log(&#96;bar() $&#123;data&#125;&#96;)</span><br><span class="line">    otherFun() &#x2F;&#x2F;内部调用</span><br><span class="line">  &#125;</span><br><span class="line">  function otherFun() &#123;</span><br><span class="line">    &#x2F;&#x2F;内部私有的函数</span><br><span class="line">    console.log(&#39;otherFun()&#39;)</span><br><span class="line">  &#125;</span><br><span class="line">  &#x2F;&#x2F;暴露行为</span><br><span class="line">  window.myModule &#x3D; &#123; foo, bar &#125; &#x2F;&#x2F;ES6写法</span><br><span class="line">&#125;)(window)</span><br></pre></td></tr></table></figure>

<p>最后得到的结果：</p>
<p><img data-src="https://cdn.jsdelivr.net/gh/huxingyi1997/my_img/img/20210420160416.png" alt="img"></p>
<ul>
<li><strong>IIFE模式增强 : 引入依赖</strong></li>
</ul>
<p>这就是现代模块实现的基石</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br></pre></td><td class="code"><pre><span class="line">&#x2F;&#x2F; module.js文件</span><br><span class="line">(function(window, $) &#123;</span><br><span class="line">  let data &#x3D; &#39;www.baidu.com&#39;</span><br><span class="line">  &#x2F;&#x2F;操作数据的函数</span><br><span class="line">  function foo() &#123;</span><br><span class="line">    &#x2F;&#x2F;用于暴露有函数</span><br><span class="line">    console.log(&#96;foo() $&#123;data&#125;&#96;)</span><br><span class="line">    $(&#39;body&#39;).css(&#39;background&#39;, &#39;red&#39;)</span><br><span class="line">  &#125;</span><br><span class="line">  function bar() &#123;</span><br><span class="line">    &#x2F;&#x2F;用于暴露有函数</span><br><span class="line">    console.log(&#96;bar() $&#123;data&#125;&#96;)</span><br><span class="line">    otherFun() &#x2F;&#x2F;内部调用</span><br><span class="line">  &#125;</span><br><span class="line">  function otherFun() &#123;</span><br><span class="line">    &#x2F;&#x2F;内部私有的函数</span><br><span class="line">    console.log(&#39;otherFun()&#39;)</span><br><span class="line">  &#125;</span><br><span class="line">  &#x2F;&#x2F;暴露行为</span><br><span class="line">  window.myModule &#x3D; &#123; foo, bar &#125;</span><br><span class="line">&#125;)(window, jQuery)</span><br><span class="line"> &#x2F;&#x2F; index.html文件</span><br><span class="line">  &lt;!-- 引入的js必须有一定顺序 --&gt;</span><br><span class="line">  &lt;script type&#x3D;&quot;text&#x2F;javascript&quot; src&#x3D;&quot;jquery-1.10.1.js&quot;&gt;&lt;&#x2F;script&gt;</span><br><span class="line">  &lt;script type&#x3D;&quot;text&#x2F;javascript&quot; src&#x3D;&quot;module.js&quot;&gt;&lt;&#x2F;script&gt;</span><br><span class="line">  &lt;script type&#x3D;&quot;text&#x2F;javascript&quot;&gt;</span><br><span class="line">    myModule.foo()</span><br><span class="line">  &lt;&#x2F;script&gt;</span><br></pre></td></tr></table></figure>

<p>上例子通过jquery方法将页面的背景颜色改成红色，所以必须先引入jQuery库，就把这个库当作参数传入。<strong>这样做除了保证模块的独立性，还使得模块之间的依赖关系变得明显</strong>。</p>
<h3 id="2-2-3-模块化的好处"><a href="#2-2-3-模块化的好处" class="headerlink" title="2.2.3 模块化的好处"></a>2.2.3 模块化的好处</h3><ul>
<li>避免命名冲突(减少命名空间污染)</li>
<li>更好的分离, 按需加载</li>
<li>更高复用性</li>
<li>高可维护性</li>
</ul>
<h3 id="2-2-4-引入多个-lt-script-gt-后出现出现问题"><a href="#2-2-4-引入多个-lt-script-gt-后出现出现问题" class="headerlink" title="2.2.4 引入多个&lt;script&gt;后出现出现问题"></a>2.2.4 引入多个<code>&lt;script&gt;</code>后出现出现问题</h3><ul>
<li>请求过多</li>
</ul>
<p>首先我们要依赖多个模块，那样就会发送多个请求，导致请求过多</p>
<ul>
<li>依赖模糊</li>
</ul>
<p>我们不知道他们的具体依赖关系是什么，也就是说很容易因为不了解他们之间的依赖关系导致加载先后顺序出错。</p>
<ul>
<li>难以维护</li>
</ul>
<p>以上两种原因就导致了很难维护，很可能出现牵一发而动全身的情况导致项目出现严重的问题。<br>模块化固然有多个好处，然而一个页面需要引入多个js文件，就会出现以上这些问题。而这些问题可以通过模块化规范来解决，下面介绍开发中最流行的commonjs, AMD, ES6, CMD规范。</p>
<h2 id="2-2-模块化规范"><a href="#2-2-模块化规范" class="headerlink" title="2.2 模块化规范"></a>2.2 模块化规范</h2><h3 id="2-2-1-CommonJS"><a href="#2-2-1-CommonJS" class="headerlink" title="2.2.1 CommonJS"></a>2.2.1 CommonJS</h3><h4 id="1-概述"><a href="#1-概述" class="headerlink" title="(1)概述"></a>(1)概述</h4><p>Node 应用由模块组成，采用 CommonJS 模块规范。每个文件就是一个模块，有自己的作用域。在一个文件里面定义的变量、函数、类，都是私有的，对其他文件不可见。<strong>在服务器端，模块的加载是运行时同步加载的；在浏览器端，模块需要提前编译打包处理。</strong></p>
<h4 id="2-特点"><a href="#2-特点" class="headerlink" title="(2)特点"></a>(2)特点</h4><ul>
<li>所有代码都运行在模块作用域，不会污染全局作用域。</li>
<li>模块可以多次加载，但是只会在第一次加载时运行一次，然后运行结果就被缓存了，以后再加载，就直接读取缓存结果。要想让模块再次运行，必须清除缓存。</li>
<li>模块加载的顺序，按照其在代码中出现的顺序。</li>
</ul>
<h4 id="3-基本语法"><a href="#3-基本语法" class="headerlink" title="(3)基本语法"></a>(3)基本语法</h4><ul>
<li>暴露模块：<code>module.exports = value</code>或<code>exports.xxx = value</code></li>
<li>引入模块：<code>require(xxx)</code>,如果是第三方模块，xxx为模块名；如果是自定义模块，xxx为模块文件路径</li>
</ul>
<p>此处我们有个疑问：<strong>CommonJS暴露的模块到底是什么?</strong> CommonJS规范规定，每个模块内部，module变量代表当前模块。这个变量是一个对象，它的exports属性（即module.exports）是对外的接口。<strong>加载某个模块，其实是加载该模块的module.exports属性</strong>。</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">&#x2F;&#x2F; example.js</span><br><span class="line">var x &#x3D; 5;</span><br><span class="line">var addX &#x3D; function (value) &#123;</span><br><span class="line">  return value + x;</span><br><span class="line">&#125;;</span><br><span class="line">module.exports.x &#x3D; x;</span><br><span class="line">module.exports.addX &#x3D; addX;</span><br></pre></td></tr></table></figure>

<p>上面代码通过module.exports输出变量x和函数addX。</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">var example &#x3D; require(&#39;.&#x2F;example.js&#39;);&#x2F;&#x2F;如果参数字符串以“.&#x2F;”开头，则表示加载的是一个位于相对路径</span><br><span class="line">console.log(example.x); &#x2F;&#x2F; 5</span><br><span class="line">console.log(example.addX(1)); &#x2F;&#x2F; 6</span><br></pre></td></tr></table></figure>

<p>require命令用于加载模块文件。<strong>require命令的基本功能是，读入并执行一个JavaScript文件，然后返回该模块的exports对象。如果没有发现指定模块，会报错</strong>。</p>
<h4 id="4-模块的加载机制"><a href="#4-模块的加载机制" class="headerlink" title="(4)模块的加载机制"></a>(4)模块的加载机制</h4><p><strong>CommonJS模块的加载机制是，输入的是被输出的值的拷贝。也就是说，一旦输出一个值，模块内部的变化就影响不到这个值</strong>。这点与ES6模块化有重大差异（下文会介绍），请看下面这个例子：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">&#x2F;&#x2F; lib.js</span><br><span class="line">var counter &#x3D; 3;</span><br><span class="line">function incCounter() &#123;</span><br><span class="line">  counter++;</span><br><span class="line">&#125;</span><br><span class="line">module.exports &#x3D; &#123;</span><br><span class="line">  counter: counter,</span><br><span class="line">  incCounter: incCounter,</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure>

<p>上面代码输出内部变量counter和改写这个变量的内部方法incCounter。</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">&#x2F;&#x2F; main.js</span><br><span class="line">var counter &#x3D; require(&#39;.&#x2F;lib&#39;).counter;</span><br><span class="line">var incCounter &#x3D; require(&#39;.&#x2F;lib&#39;).incCounter;</span><br><span class="line"></span><br><span class="line">console.log(counter);  &#x2F;&#x2F; 3</span><br><span class="line">incCounter();</span><br><span class="line">console.log(counter); &#x2F;&#x2F; 3</span><br></pre></td></tr></table></figure>

<p>上面代码说明，counter输出以后，lib.js模块内部的变化就影响不到counter了。<strong>这是因为counter是一个原始类型的值，会被缓存。除非写成一个函数，才能得到内部变动后的值</strong>。</p>
<h4 id="5-服务器端实现"><a href="#5-服务器端实现" class="headerlink" title="(5)服务器端实现"></a>(5)服务器端实现</h4><h4 id="①下载安装node-js"><a href="#①下载安装node-js" class="headerlink" title="①下载安装node.js"></a>①下载安装node.js</h4><h4 id="②创建项目结构"><a href="#②创建项目结构" class="headerlink" title="②创建项目结构"></a>②创建项目结构</h4><p><strong>注意：用npm init 自动生成package.json时，package name(包名)不能有中文和大写</strong></p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line">|-modules</span><br><span class="line">  |-module1.js</span><br><span class="line">  |-module2.js</span><br><span class="line">  |-module3.js</span><br><span class="line">|-app.js</span><br><span class="line">|-package.json</span><br><span class="line">  &#123;</span><br><span class="line">    &quot;name&quot;: &quot;commonJS-node&quot;,</span><br><span class="line">    &quot;version&quot;: &quot;1.0.0&quot;</span><br><span class="line">  &#125;</span><br></pre></td></tr></table></figure>

<h4 id="③下载第三方模块"><a href="#③下载第三方模块" class="headerlink" title="③下载第三方模块"></a>③下载第三方模块</h4><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">npm install uniq --save &#x2F;&#x2F; 用于数组去重</span><br></pre></td></tr></table></figure>

<h4 id="④定义模块代码"><a href="#④定义模块代码" class="headerlink" title="④定义模块代码"></a>④定义模块代码</h4><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br></pre></td><td class="code"><pre><span class="line">&#x2F;&#x2F;module1.js</span><br><span class="line">module.exports &#x3D; &#123;</span><br><span class="line">  msg: &#39;module1&#39;,</span><br><span class="line">  foo() &#123;</span><br><span class="line">    console.log(this.msg)</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br><span class="line">&#x2F;&#x2F;module2.js</span><br><span class="line">module.exports &#x3D; function() &#123;</span><br><span class="line">  console.log(&#39;module2&#39;)</span><br><span class="line">&#125;</span><br><span class="line">&#x2F;&#x2F;module3.js</span><br><span class="line">exports.foo &#x3D; function() &#123;</span><br><span class="line">  console.log(&#39;foo() module3&#39;)</span><br><span class="line">&#125;</span><br><span class="line">exports.arr &#x3D; [1, 2, 3, 3, 2]</span><br><span class="line">&#x2F;&#x2F; app.js文件</span><br><span class="line">&#x2F;&#x2F; 引入第三方库，应该放置在最前面</span><br><span class="line">let uniq &#x3D; require(&#39;uniq&#39;)</span><br><span class="line">let module1 &#x3D; require(&#39;.&#x2F;modules&#x2F;module1&#39;)</span><br><span class="line">let module2 &#x3D; require(&#39;.&#x2F;modules&#x2F;module2&#39;)</span><br><span class="line">let module3 &#x3D; require(&#39;.&#x2F;modules&#x2F;module3&#39;)</span><br><span class="line"></span><br><span class="line">module1.foo() &#x2F;&#x2F;module1</span><br><span class="line">module2() &#x2F;&#x2F;module2</span><br><span class="line">module3.foo() &#x2F;&#x2F;foo() module3</span><br><span class="line">console.log(uniq(module3.arr)) &#x2F;&#x2F;[ 1, 2, 3 ]</span><br></pre></td></tr></table></figure>

<h4 id="⑤通过node运行app-js"><a href="#⑤通过node运行app-js" class="headerlink" title="⑤通过node运行app.js"></a>⑤通过node运行app.js</h4><p>命令行输入<code>node app.js</code>，运行JS文件</p>
<h4 id="6-浏览器端实现-借助Browserify"><a href="#6-浏览器端实现-借助Browserify" class="headerlink" title="(6)浏览器端实现(借助Browserify)"></a>(6)浏览器端实现(借助Browserify)</h4><h4 id="①创建项目结构"><a href="#①创建项目结构" class="headerlink" title="①创建项目结构"></a>①创建项目结构</h4><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line">|-js</span><br><span class="line">  |-dist &#x2F;&#x2F;打包生成文件的目录</span><br><span class="line">  |-src &#x2F;&#x2F;源码所在的目录</span><br><span class="line">    |-module1.js</span><br><span class="line">    |-module2.js</span><br><span class="line">    |-module3.js</span><br><span class="line">    |-app.js &#x2F;&#x2F;应用主源文件</span><br><span class="line">|-index.html &#x2F;&#x2F;运行于浏览器上</span><br><span class="line">|-package.json</span><br><span class="line">  &#123;</span><br><span class="line">    &quot;name&quot;: &quot;browserify-test&quot;,</span><br><span class="line">    &quot;version&quot;: &quot;1.0.0&quot;</span><br><span class="line">  &#125;</span><br></pre></td></tr></table></figure>

<h4 id="②下载browserify"><a href="#②下载browserify" class="headerlink" title="②下载browserify"></a>②下载browserify</h4><ul>
<li>全局: npm install browserify -g</li>
<li>局部: npm install browserify –save-dev</li>
</ul>
<h4 id="③定义模块代码-同服务器端"><a href="#③定义模块代码-同服务器端" class="headerlink" title="③定义模块代码(同服务器端)"></a>③定义模块代码(同服务器端)</h4><p>注意：<code>index.html</code>文件要运行在浏览器上，需要借助browserify将<code>app.js</code>文件打包编译，如果直接在<code>index.html</code>引入<code>app.js</code>就会报错！</p>
<h4 id="④打包处理js"><a href="#④打包处理js" class="headerlink" title="④打包处理js"></a>④打包处理js</h4><p>根目录下运行<code>browserify js/src/app.js -o js/dist/bundle.js</code></p>
<h4 id="⑤页面使用引入"><a href="#⑤页面使用引入" class="headerlink" title="⑤页面使用引入"></a>⑤页面使用引入</h4><p>在index.html文件中引入<code>&lt;script type=&quot;text/javascript&quot; src=&quot;js/dist/bundle.js&quot;&gt;&lt;/script&gt;</code></p>
<h3 id="2-2-2-AMD"><a href="#2-2-2-AMD" class="headerlink" title="2.2.2 AMD"></a>2.2.2 AMD</h3><p>CommonJS规范加载模块是同步的，也就是说，只有加载完成，才能执行后面的操作。AMD规范则是非同步加载模块，允许指定回调函数。由于Node.js主要用于服务器编程，模块文件一般都已经存在于本地硬盘，所以加载起来比较快，不用考虑非同步加载的方式，所以CommonJS规范比较适用。但是，<strong>如果是浏览器环境，要从服务器端加载模块，这时就必须采用非同步模式，因此浏览器端一般采用AMD规范</strong>。此外AMD规范比CommonJS规范在浏览器端实现要来着早。</p>
<h4 id="1-AMD规范基本语法"><a href="#1-AMD规范基本语法" class="headerlink" title="(1)AMD规范基本语法"></a>(1)AMD规范基本语法</h4><p><strong>定义暴露模块</strong>:</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">&#x2F;&#x2F;定义没有依赖的模块</span><br><span class="line">define(function()&#123;</span><br><span class="line">   return 模块</span><br><span class="line">&#125;)</span><br><span class="line">&#x2F;&#x2F;定义有依赖的模块</span><br><span class="line">define([&#39;module1&#39;, &#39;module2&#39;], function(m1, m2)&#123;</span><br><span class="line">   return 模块</span><br><span class="line">&#125;)</span><br></pre></td></tr></table></figure>

<p><strong>引入使用模块</strong>:</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">require([&#39;module1&#39;, &#39;module2&#39;], function(m1, m2)&#123;</span><br><span class="line">   使用m1&#x2F;m2</span><br><span class="line">&#125;)</span><br></pre></td></tr></table></figure>

<h4 id="2-未使用AMD规范与使用require-js"><a href="#2-未使用AMD规范与使用require-js" class="headerlink" title="(2)未使用AMD规范与使用require.js"></a>(2)未使用AMD规范与使用require.js</h4><p>通过比较两者的实现方法，来说明使用AMD规范的好处。</p>
<ul>
<li>未使用AMD规范</li>
</ul>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br></pre></td><td class="code"><pre><span class="line">&#x2F;&#x2F; dataService.js文件</span><br><span class="line">(function (window) &#123;</span><br><span class="line">  let msg &#x3D; &#39;www.baidu.com&#39;</span><br><span class="line">  function getMsg() &#123;</span><br><span class="line">    return msg.toUpperCase()</span><br><span class="line">  &#125;</span><br><span class="line">  window.dataService &#x3D; &#123;getMsg&#125;</span><br><span class="line">&#125;)(window)</span><br><span class="line"> &#x2F;&#x2F; alerter.js文件</span><br><span class="line">(function (window, dataService) &#123;</span><br><span class="line">  let name &#x3D; &#39;Tom&#39;</span><br><span class="line">  function showMsg() &#123;</span><br><span class="line">    alert(dataService.getMsg() + &#39;, &#39; + name)</span><br><span class="line">  &#125;</span><br><span class="line">  window.alerter &#x3D; &#123;showMsg&#125;</span><br><span class="line">&#125;)(window, dataService)</span><br><span class="line">&#x2F;&#x2F; main.js文件</span><br><span class="line">(function (alerter) &#123;</span><br><span class="line">  alerter.showMsg()</span><br><span class="line">&#125;)(alerter)</span><br><span class="line">&#x2F;&#x2F; index.html文件</span><br><span class="line">&lt;div&gt;&lt;h1&gt;Modular Demo 1: 未使用AMD(require.js)&lt;&#x2F;h1&gt;&lt;&#x2F;div&gt;</span><br><span class="line">&lt;script type&#x3D;&quot;text&#x2F;javascript&quot; src&#x3D;&quot;js&#x2F;modules&#x2F;dataService.js&quot;&gt;&lt;&#x2F;script&gt;</span><br><span class="line">&lt;script type&#x3D;&quot;text&#x2F;javascript&quot; src&#x3D;&quot;js&#x2F;modules&#x2F;alerter.js&quot;&gt;&lt;&#x2F;script&gt;</span><br><span class="line">&lt;script type&#x3D;&quot;text&#x2F;javascript&quot; src&#x3D;&quot;js&#x2F;main.js&quot;&gt;&lt;&#x2F;script&gt;</span><br></pre></td></tr></table></figure>

<p>最后得到如下结果：<br><img data-src="https://cdn.jsdelivr.net/gh/huxingyi1997/my_img/img/20210420160559.png" alt="img"></p>
<p>这种方式缺点很明显：<strong>首先会发送多个请求，其次引入的js文件顺序不能搞错，否则会报错！</strong></p>
<ul>
<li>使用require.js</li>
</ul>
<p>RequireJS是一个工具库，主要用于客户端的模块管理。它的模块管理遵守AMD规范，<strong>RequireJS的基本思想是，通过define方法，将代码定义为模块；通过require方法，实现代码的模块加载</strong>。<br>接下来介绍AMD规范在浏览器实现的步骤：</p>
<h4 id="①下载require-js-并引入"><a href="#①下载require-js-并引入" class="headerlink" title="①下载require.js, 并引入"></a>①下载require.js, 并引入</h4><ul>
<li>官网: <code>http://www.requirejs.cn/</code></li>
<li>github : <code>https://github.com/requirejs/requirejs</code></li>
</ul>
<p>然后将require.js导入项目: js/libs/require.js</p>
<h4 id="②创建项目结构-1"><a href="#②创建项目结构-1" class="headerlink" title="②创建项目结构"></a>②创建项目结构</h4><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">|-js</span><br><span class="line">  |-libs</span><br><span class="line">    |-require.js</span><br><span class="line">  |-modules</span><br><span class="line">    |-alerter.js</span><br><span class="line">    |-dataService.js</span><br><span class="line">  |-main.js</span><br><span class="line">|-index.html</span><br></pre></td></tr></table></figure>

<h4 id="③定义require-js的模块代码"><a href="#③定义require-js的模块代码" class="headerlink" title="③定义require.js的模块代码"></a>③定义require.js的模块代码</h4><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br></pre></td><td class="code"><pre><span class="line">&#x2F;&#x2F; dataService.js文件 </span><br><span class="line">&#x2F;&#x2F; 定义没有依赖的模块</span><br><span class="line">define(function() &#123;</span><br><span class="line">  let msg &#x3D; &#39;www.baidu.com&#39;</span><br><span class="line">  function getMsg() &#123;</span><br><span class="line">    return msg.toUpperCase()</span><br><span class="line">  &#125;</span><br><span class="line">  return &#123; getMsg &#125; &#x2F;&#x2F; 暴露模块</span><br><span class="line">&#125;)</span><br><span class="line">&#x2F;&#x2F;alerter.js文件</span><br><span class="line">&#x2F;&#x2F; 定义有依赖的模块</span><br><span class="line">define([&#39;dataService&#39;], function(dataService) &#123;</span><br><span class="line">  let name &#x3D; &#39;Tom&#39;</span><br><span class="line">  function showMsg() &#123;</span><br><span class="line">    alert(dataService.getMsg() + &#39;, &#39; + name)</span><br><span class="line">  &#125;</span><br><span class="line">  &#x2F;&#x2F; 暴露模块</span><br><span class="line">  return &#123; showMsg &#125;</span><br><span class="line">&#125;)</span><br><span class="line">&#x2F;&#x2F; main.js文件</span><br><span class="line">(function() &#123;</span><br><span class="line">  require.config(&#123;</span><br><span class="line">    baseUrl: &#39;js&#x2F;&#39;, &#x2F;&#x2F;基本路径 出发点在根目录下</span><br><span class="line">    paths: &#123;</span><br><span class="line">      &#x2F;&#x2F;映射: 模块标识名: 路径</span><br><span class="line">      alerter: &#39;.&#x2F;modules&#x2F;alerter&#39;, &#x2F;&#x2F;此处不能写成alerter.js,会报错</span><br><span class="line">      dataService: &#39;.&#x2F;modules&#x2F;dataService&#39;</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;)</span><br><span class="line">  require([&#39;alerter&#39;], function(alerter) &#123;</span><br><span class="line">    alerter.showMsg()</span><br><span class="line">  &#125;)</span><br><span class="line">&#125;)()</span><br><span class="line">&#x2F;&#x2F; index.html文件</span><br><span class="line">&lt;!DOCTYPE html&gt;</span><br><span class="line">&lt;html&gt;</span><br><span class="line">  &lt;head&gt;</span><br><span class="line">    &lt;title&gt;Modular Demo&lt;&#x2F;title&gt;</span><br><span class="line">  &lt;&#x2F;head&gt;</span><br><span class="line">  &lt;body&gt;</span><br><span class="line">    &lt;!-- 引入require.js并指定js主文件的入口 --&gt;</span><br><span class="line">    &lt;script data-main&#x3D;&quot;js&#x2F;main&quot; src&#x3D;&quot;js&#x2F;libs&#x2F;require.js&quot;&gt;&lt;&#x2F;script&gt;</span><br><span class="line">  &lt;&#x2F;body&gt;</span><br><span class="line">&lt;&#x2F;html&gt;</span><br></pre></td></tr></table></figure>

<h4 id="④页面引入require-js模块"><a href="#④页面引入require-js模块" class="headerlink" title="④页面引入require.js模块:"></a>④页面引入require.js模块:</h4><p>在index.html引入 <code>&lt;script data-main=&quot;js/main&quot; src=&quot;js/libs/require.js&quot;&gt;&lt;/script&gt;</code></p>
<p><strong>此外在项目中如何引入第三方库？</strong>只需在上面代码的基础稍作修改：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br></pre></td><td class="code"><pre><span class="line">&#x2F;&#x2F; alerter.js文件</span><br><span class="line">define([&#39;dataService&#39;, &#39;jquery&#39;], function(dataService, $) &#123;</span><br><span class="line">  let name &#x3D; &#39;Tom&#39;</span><br><span class="line">  function showMsg() &#123;</span><br><span class="line">    alert(dataService.getMsg() + &#39;, &#39; + name)</span><br><span class="line">  &#125;</span><br><span class="line">  $(&#39;body&#39;).css(&#39;background&#39;, &#39;green&#39;)</span><br><span class="line">  &#x2F;&#x2F; 暴露模块</span><br><span class="line">  return &#123; showMsg &#125;</span><br><span class="line">&#125;)</span><br><span class="line">&#x2F;&#x2F; main.js文件</span><br><span class="line">(function() &#123;</span><br><span class="line">  require.config(&#123;</span><br><span class="line">    baseUrl: &#39;js&#x2F;&#39;, &#x2F;&#x2F;基本路径 出发点在根目录下</span><br><span class="line">    paths: &#123;</span><br><span class="line">      &#x2F;&#x2F;自定义模块</span><br><span class="line">      alerter: &#39;.&#x2F;modules&#x2F;alerter&#39;, &#x2F;&#x2F;此处不能写成alerter.js,会报错</span><br><span class="line">      dataService: &#39;.&#x2F;modules&#x2F;dataService&#39;,</span><br><span class="line">      &#x2F;&#x2F; 第三方库模块</span><br><span class="line">      jquery: &#39;.&#x2F;libs&#x2F;jquery-1.10.1&#39; &#x2F;&#x2F;注意：写成jQuery会报错</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;)</span><br><span class="line">  require([&#39;alerter&#39;], function(alerter) &#123;</span><br><span class="line">    alerter.showMsg()</span><br><span class="line">  &#125;)</span><br><span class="line">&#125;)()</span><br></pre></td></tr></table></figure>

<p>上例是在alerter.js文件中引入jQuery第三方库，main.js文件也要有相应的路径配置。<br><strong>小结</strong>：通过两者的比较，可以得出<strong>AMD模块定义的方法非常清晰，不会污染全局环境，能够清楚地显示依赖关系</strong>。AMD模式可以用于浏览器环境，并且允许非同步加载模块，也可以根据需要动态加载模块。</p>
<h3 id="2-2-3-CMD"><a href="#2-2-3-CMD" class="headerlink" title="2.2.3 CMD"></a>2.2.3 CMD</h3><p>CMD规范专门用于浏览器端，模块的加载是异步的，模块使用时才会加载执行。CMD规范整合了CommonJS和AMD规范的特点。在 Sea.js 中，所有 JavaScript 模块都遵循 CMD模块定义规范。</p>
<h4 id="1-CMD规范基本语法"><a href="#1-CMD规范基本语法" class="headerlink" title="(1)CMD规范基本语法"></a>(1)CMD规范基本语法</h4><p><strong>定义暴露模块：</strong></p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line">&#x2F;&#x2F;定义没有依赖的模块</span><br><span class="line">define(function(require, exports, module)&#123;</span><br><span class="line">  exports.xxx &#x3D; value</span><br><span class="line">  module.exports &#x3D; value</span><br><span class="line">&#125;)</span><br><span class="line">&#x2F;&#x2F;定义有依赖的模块</span><br><span class="line">define(function(require, exports, module)&#123;</span><br><span class="line">  &#x2F;&#x2F;引入依赖模块(同步)</span><br><span class="line">  var module2 &#x3D; require(&#39;.&#x2F;module2&#39;)</span><br><span class="line">  &#x2F;&#x2F;引入依赖模块(异步)</span><br><span class="line">    require.async(&#39;.&#x2F;module3&#39;, function (m3) &#123;</span><br><span class="line">    &#125;)</span><br><span class="line">  &#x2F;&#x2F;暴露模块</span><br><span class="line">  exports.xxx &#x3D; value</span><br><span class="line">&#125;)</span><br></pre></td></tr></table></figure>

<p><strong>引入使用模块：</strong></p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">define(function (require) &#123;</span><br><span class="line">  var m1 &#x3D; require(&#39;.&#x2F;module1&#39;)</span><br><span class="line">  var m4 &#x3D; require(&#39;.&#x2F;module4&#39;)</span><br><span class="line">  m1.show()</span><br><span class="line">  m4.show()</span><br><span class="line">&#125;)</span><br></pre></td></tr></table></figure>

<h4 id="2-sea-js简单使用教程"><a href="#2-sea-js简单使用教程" class="headerlink" title="(2)sea.js简单使用教程"></a>(2)sea.js简单使用教程</h4><h4 id="①下载sea-js-并引入"><a href="#①下载sea-js-并引入" class="headerlink" title="①下载sea.js, 并引入"></a>①下载sea.js, 并引入</h4><ul>
<li>官网: <a target="_blank" rel="noopener" href="http://seajs.org/">http://seajs.org/</a></li>
<li>github : <a target="_blank" rel="noopener" href="https://github.com/seajs/seajs">https://github.com/seajs/seajs</a></li>
</ul>
<p>然后将sea.js导入项目: js/libs/sea.js</p>
<h4 id="②创建项目结构-2"><a href="#②创建项目结构-2" class="headerlink" title="②创建项目结构"></a>②创建项目结构</h4><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line">|-js</span><br><span class="line">  |-libs</span><br><span class="line">    |-sea.js</span><br><span class="line">  |-modules</span><br><span class="line">    |-module1.js</span><br><span class="line">    |-module2.js</span><br><span class="line">    |-module3.js</span><br><span class="line">    |-module4.js</span><br><span class="line">    |-main.js</span><br><span class="line">|-index.html</span><br></pre></td></tr></table></figure>

<h4 id="③定义sea-js的模块代码"><a href="#③定义sea-js的模块代码" class="headerlink" title="③定义sea.js的模块代码"></a>③定义sea.js的模块代码</h4><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br></pre></td><td class="code"><pre><span class="line">&#x2F;&#x2F; module1.js文件</span><br><span class="line">define(function (require, exports, module) &#123;</span><br><span class="line">  &#x2F;&#x2F;内部变量数据</span><br><span class="line">  var data &#x3D; &#39;atguigu.com&#39;</span><br><span class="line">  &#x2F;&#x2F;内部函数</span><br><span class="line">  function show() &#123;</span><br><span class="line">    console.log(&#39;module1 show() &#39; + data)</span><br><span class="line">  &#125;</span><br><span class="line">  &#x2F;&#x2F;向外暴露</span><br><span class="line">  exports.show &#x3D; show</span><br><span class="line">&#125;)</span><br><span class="line">&#x2F;&#x2F; module2.js文件</span><br><span class="line">define(function (require, exports, module) &#123;</span><br><span class="line">  module.exports &#x3D; &#123;</span><br><span class="line">    msg: &#39;I Will Back&#39;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;)</span><br><span class="line">&#x2F;&#x2F; module3.js文件</span><br><span class="line">define(function(require, exports, module) &#123;</span><br><span class="line">  const API_KEY &#x3D; &#39;abc123&#39;</span><br><span class="line">  exports.API_KEY &#x3D; API_KEY</span><br><span class="line">&#125;)</span><br><span class="line">&#x2F;&#x2F; module4.js文件</span><br><span class="line">define(function (require, exports, module) &#123;</span><br><span class="line">  &#x2F;&#x2F;引入依赖模块(同步)</span><br><span class="line">  var module2 &#x3D; require(&#39;.&#x2F;module2&#39;)</span><br><span class="line">  function show() &#123;</span><br><span class="line">    console.log(&#39;module4 show() &#39; + module2.msg)</span><br><span class="line">  &#125;</span><br><span class="line">  exports.show &#x3D; show</span><br><span class="line">  &#x2F;&#x2F;引入依赖模块(异步)</span><br><span class="line">  require.async(&#39;.&#x2F;module3&#39;, function (m3) &#123;</span><br><span class="line">    console.log(&#39;异步引入依赖模块3  &#39; + m3.API_KEY)</span><br><span class="line">  &#125;)</span><br><span class="line">&#125;)</span><br><span class="line">&#x2F;&#x2F; main.js文件</span><br><span class="line">define(function (require) &#123;</span><br><span class="line">  var m1 &#x3D; require(&#39;.&#x2F;module1&#39;)</span><br><span class="line">  var m4 &#x3D; require(&#39;.&#x2F;module4&#39;)</span><br><span class="line">  m1.show()</span><br><span class="line">  m4.show()</span><br><span class="line">&#125;)</span><br></pre></td></tr></table></figure>

<h4 id="④在index-html中引入"><a href="#④在index-html中引入" class="headerlink" title="④在index.html中引入"></a>④在index.html中引入</h4><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">&lt;script type&#x3D;&quot;text&#x2F;javascript&quot; src&#x3D;&quot;js&#x2F;libs&#x2F;sea.js&quot;&gt;&lt;&#x2F;script&gt;</span><br><span class="line">&lt;script type&#x3D;&quot;text&#x2F;javascript&quot;&gt;</span><br><span class="line">  seajs.use(&#39;.&#x2F;js&#x2F;modules&#x2F;main&#39;)</span><br><span class="line">&lt;&#x2F;script&gt;</span><br></pre></td></tr></table></figure>

<p>最后得到结果如下：</p>
<p><img data-src="https://cdn.jsdelivr.net/gh/huxingyi1997/my_img/img/20210420160712.png" alt="img"></p>
<h3 id="2-2-4-ES6模块化"><a href="#2-2-4-ES6模块化" class="headerlink" title="2.2.4 ES6模块化"></a>2.2.4 ES6模块化</h3><p>ES6 模块的设计思想是尽量的静态化，使得编译时就能确定模块的依赖关系，以及输入和输出的变量。CommonJS 和 AMD 模块，都只能在运行时确定这些东西。比如，CommonJS 模块就是对象，输入时必须查找对象属性。</p>
<h4 id="1-ES6模块化语法"><a href="#1-ES6模块化语法" class="headerlink" title="(1)ES6模块化语法"></a>(1)ES6模块化语法</h4><p>export命令用于规定模块的对外接口，import命令用于输入其他模块提供的功能。</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">&#x2F;** 定义模块 math.js **&#x2F;</span><br><span class="line">var basicNum &#x3D; 0;</span><br><span class="line">var add &#x3D; function (a, b) &#123;</span><br><span class="line">    return a + b;</span><br><span class="line">&#125;;</span><br><span class="line">export &#123; basicNum, add &#125;;</span><br><span class="line">&#x2F;** 引用模块 **&#x2F;</span><br><span class="line">import &#123; basicNum, add &#125; from &#39;.&#x2F;math&#39;;</span><br><span class="line">function test(ele) &#123;</span><br><span class="line">    ele.textContent &#x3D; add(99 + basicNum);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>如上例所示，使用import命令的时候，用户需要知道所要加载的变量名或函数名，否则无法加载。为了给用户提供方便，让他们不用阅读文档就能加载模块，就要用到export default命令，为模块指定默认输出。</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">&#x2F;&#x2F; export-default.js</span><br><span class="line">export default function () &#123;</span><br><span class="line">  console.log(&#39;foo&#39;);</span><br><span class="line">&#125;</span><br><span class="line">&#x2F;&#x2F; import-default.js</span><br><span class="line">import customName from &#39;.&#x2F;export-default&#39;;</span><br><span class="line">customName(); &#x2F;&#x2F; &#39;foo&#39;</span><br></pre></td></tr></table></figure>

<p>模块默认输出, 其他模块加载该模块时，import命令可以为该匿名函数指定任意名字。</p>
<h4 id="2-ES6-模块与-CommonJS-模块的差异"><a href="#2-ES6-模块与-CommonJS-模块的差异" class="headerlink" title="(2)ES6 模块与 CommonJS 模块的差异"></a>(2)ES6 模块与 CommonJS 模块的差异</h4><p>它们有两个重大差异：</p>
<p><strong>① CommonJS 模块输出的是一个值的拷贝，ES6 模块输出的是值的引用</strong>。</p>
<p><strong>② CommonJS 模块是运行时加载，ES6 模块是编译时输出接口</strong>。</p>
<p>第二个差异是因为 CommonJS 加载的是一个对象（即module.exports属性），该对象只有在脚本运行完才会生成。而 ES6 模块不是对象，它的对外接口只是一种静态定义，在代码静态解析阶段就会生成。</p>
<p>下面重点解释第一个差异，我们还是举上面那个CommonJS模块的加载机制例子:</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line">&#x2F;&#x2F; lib.js</span><br><span class="line">export let counter &#x3D; 3;</span><br><span class="line">export function incCounter() &#123;</span><br><span class="line">  counter++;</span><br><span class="line">&#125;</span><br><span class="line">&#x2F;&#x2F; main.js</span><br><span class="line">import &#123; counter, incCounter &#125; from &#39;.&#x2F;lib&#39;;</span><br><span class="line">console.log(counter); &#x2F;&#x2F; 3</span><br><span class="line">incCounter();</span><br><span class="line">console.log(counter); &#x2F;&#x2F; 4</span><br></pre></td></tr></table></figure>

<p>ES6 模块的运行机制与 CommonJS 不一样。<strong>ES6 模块是动态引用，并且不会缓存值，模块里面的变量绑定其所在的模块</strong>。</p>
<h4 id="3-ES6-Babel-Browserify使用教程"><a href="#3-ES6-Babel-Browserify使用教程" class="headerlink" title="(3) ES6-Babel-Browserify使用教程"></a>(3) ES6-Babel-Browserify使用教程</h4><p>简单来说就一句话：<strong>使用Babel将ES6编译为ES5代码，使用Browserify编译打包js</strong>。</p>
<h4 id="①定义package-json文件"><a href="#①定义package-json文件" class="headerlink" title="①定义package.json文件"></a>①定义package.json文件</h4><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  &quot;name&quot; : &quot;es6-babel-browserify&quot;,</span><br><span class="line">  &quot;version&quot; : &quot;1.0.0&quot;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<h4 id="②安装babel-cli-babel-preset-es2015和browserify"><a href="#②安装babel-cli-babel-preset-es2015和browserify" class="headerlink" title="②安装babel-cli, babel-preset-es2015和browserify"></a>②安装babel-cli, babel-preset-es2015和browserify</h4><ul>
<li><p>npm install babel-cli browserify -g</p>
</li>
<li><p>npm install babel-preset-es2015 –save-dev</p>
</li>
<li><p>preset 预设(将es6转换成es5的所有插件打包)</p>
</li>
</ul>
<h4 id="③定义-babelrc文件"><a href="#③定义-babelrc文件" class="headerlink" title="③定义.babelrc文件"></a>③定义.babelrc文件</h4><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  &quot;presets&quot;: [&quot;es2015&quot;]</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<h4 id="④定义模块代码-1"><a href="#④定义模块代码-1" class="headerlink" title="④定义模块代码"></a>④定义模块代码</h4><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br></pre></td><td class="code"><pre><span class="line">&#x2F;&#x2F;module1.js文件</span><br><span class="line">&#x2F;&#x2F; 分别暴露</span><br><span class="line">export function foo() &#123;</span><br><span class="line">  console.log(&#39;foo() module1&#39;)</span><br><span class="line">&#125;</span><br><span class="line">export function bar() &#123;</span><br><span class="line">  console.log(&#39;bar() module1&#39;)</span><br><span class="line">&#125;</span><br><span class="line">&#x2F;&#x2F;module2.js文件</span><br><span class="line">&#x2F;&#x2F; 统一暴露</span><br><span class="line">function fun1() &#123;</span><br><span class="line">  console.log(&#39;fun1() module2&#39;)</span><br><span class="line">&#125;</span><br><span class="line">function fun2() &#123;</span><br><span class="line">  console.log(&#39;fun2() module2&#39;)</span><br><span class="line">&#125;</span><br><span class="line">export &#123; fun1, fun2 &#125;</span><br><span class="line">&#x2F;&#x2F;module3.js文件</span><br><span class="line">&#x2F;&#x2F; 默认暴露 可以暴露任意数据类项，暴露什么数据，接收到就是什么数据</span><br><span class="line">export default () &#x3D;&gt; &#123;</span><br><span class="line">  console.log(&#39;默认暴露&#39;)</span><br><span class="line">&#125;</span><br><span class="line">&#x2F;&#x2F; app.js文件</span><br><span class="line">import &#123; foo, bar &#125; from &#39;.&#x2F;module1&#39;</span><br><span class="line">import &#123; fun1, fun2 &#125; from &#39;.&#x2F;module2&#39;</span><br><span class="line">import module3 from &#39;.&#x2F;module3&#39;</span><br><span class="line">foo()</span><br><span class="line">bar()</span><br><span class="line">fun1()</span><br><span class="line">fun2()</span><br><span class="line">module3()</span><br></pre></td></tr></table></figure>

<h4 id="⑤-编译并在index-html中引入"><a href="#⑤-编译并在index-html中引入" class="headerlink" title="⑤ 编译并在index.html中引入"></a>⑤ 编译并在index.html中引入</h4><ul>
<li>使用Babel将ES6编译为ES5代码(但包含CommonJS语法) : <code>babel js/src -d js/lib</code></li>
<li>使用Browserify编译js : <code>browserify js/lib/app.js -o js/lib/bundle.js</code></li>
</ul>
<p>然后在index.html文件中引入</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">&lt;script type&#x3D;&quot;text&#x2F;javascript&quot; src&#x3D;&quot;js&#x2F;lib&#x2F;bundle.js&quot;&gt;&lt;&#x2F;script&gt;</span><br></pre></td></tr></table></figure>

<p>最后得到如下结果：</p>
<p><img data-src="https://cdn.jsdelivr.net/gh/huxingyi1997/my_img/img/20210420160730.png" alt="img"></p>
<p><strong>此外第三方库(以jQuery为例)如何引入呢</strong>？<br>首先安装依赖<code>npm install jquery@1</code><br>然后在app.js文件中引入</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line">&#x2F;&#x2F;app.js文件</span><br><span class="line">import &#123; foo, bar &#125; from &#39;.&#x2F;module1&#39;</span><br><span class="line">import &#123; fun1, fun2 &#125; from &#39;.&#x2F;module2&#39;</span><br><span class="line">import module3 from &#39;.&#x2F;module3&#39;</span><br><span class="line">import $ from &#39;jquery&#39;</span><br><span class="line"></span><br><span class="line">foo()</span><br><span class="line">bar()</span><br><span class="line">fun1()</span><br><span class="line">fun2()</span><br><span class="line">module3()</span><br><span class="line">$(&#39;body&#39;).css(&#39;background&#39;, &#39;green&#39;)</span><br></pre></td></tr></table></figure>

<h2 id="2-3-总结"><a href="#2-3-总结" class="headerlink" title="2.3 总结"></a>2.3 总结</h2><ul>
<li>CommonJS规范主要用于服务端编程，加载模块是同步的，这并不适合在浏览器环境，因为同步意味着阻塞加载，浏览器资源是异步加载的，因此有了AMD CMD解决方案。</li>
<li>AMD规范在浏览器环境中异步加载模块，而且可以并行加载多个模块。不过，AMD规范开发成本高，代码的阅读和书写比较困难，模块定义方式的语义不顺畅。</li>
<li>CMD规范与AMD规范很相似，都用于浏览器编程，依赖就近，延迟执行，可以很容易在Node.js中运行。不过，依赖SPM 打包，模块的加载逻辑偏重</li>
<li><strong>ES6 在语言标准的层面上，实现了模块功能，而且实现得相当简单，完全可以取代 CommonJS 和 AMD 规范，成为浏览器和服务器通用的模块解决方案</strong>。</li>
</ul>
<h1 id="3-Tree-Shaking"><a href="#3-Tree-Shaking" class="headerlink" title="3.Tree-Shaking"></a>3.Tree-Shaking</h1><h2 id="3-1-什么是Tree-shaking"><a href="#3-1-什么是Tree-shaking" class="headerlink" title="3.1 什么是Tree-shaking"></a>3.1 什么是Tree-shaking</h2><p><img data-src="https://cdn.jsdelivr.net/gh/huxingyi1997/my_img/img/20210420164618.webp" alt="img"></p>
<p>先来看一下Tree-shaking原始的本意</p>
<p><img data-src="https://cdn.jsdelivr.net/gh/huxingyi1997/my_img/img/20210420164700.gif" alt="img"></p>
<p>上图形象的解释了Tree-shaking 的本意，本文所说的前端中的tree-shaking可以理解为通过工具”摇”我们的JS文件，将其中用不到的代码”摇”掉，是一个性能优化的范畴。具体来说，在 webpack 项目中，有一个入口文件，相当于一棵树的主干，入口文件有很多依赖的模块，相当于树枝。实际情况中，虽然依赖了某个模块，但其实只使用其中的某些功能。通过 tree-shaking，将没有使用的模块摇掉，这样来达到删除无用代码的目的。</p>
<p><img data-src="https://cdn.jsdelivr.net/gh/huxingyi1997/my_img/img/20210420164718.webp" alt="img"></p>
<p>Tree-shaking 较早由 Rich_Harris 的 rollup 实现，后来，webpack2 也增加了tree-shaking 的功能。其实在更早，google closure compiler 也做过类似的事情。三个工具的效果和使用各不相同，使用方法可以通过官网文档去了解，三者的效果对比，后文会详细介绍。</p>
<h2 id="3-2-tree-shaking的原理"><a href="#3-2-tree-shaking的原理" class="headerlink" title="3.2 tree-shaking的原理"></a>3.2 tree-shaking的原理</h2><p><img data-src="https://cdn.jsdelivr.net/gh/huxingyi1997/my_img/img/20210420164736.webp" alt="img"></p>
<p>Tree-shaking的本质是消除无用的js代码。无用代码消除在广泛存在于传统的编程语言编译器中，编译器可以判断出某些代码根本不影响输出，然后消除这些代码，这个称之为DCE（dead code elimination）。</p>
<p>Tree-shaking 是 DCE 的一种新的实现，Javascript同传统的编程语言不同的是，javascript绝大多数情况需要通过网络进行加载，然后执行，加载的文件大小越小，整体执行时间更短，所以去除无用代码以减少文件体积，对javascript来说更有意义。</p>
<p>Tree-shaking 和传统的 DCE的方法又不太一样，传统的DCE 消灭不可能执行的代码，而Tree-shaking 更关注宇消除没有用到的代码。下面详细介绍一下DCE和Tree-shaking。</p>
<p><strong>（1）先来看一下DCE消除大法</strong></p>
<p><img data-src="https://cdn.jsdelivr.net/gh/huxingyi1997/my_img/img/20210420164745.webp" alt="img"></p>
<p>Dead Code 一般具有以下几个特征</p>
<p>•代码不会被执行，不可到达</p>
<p>•代码执行的结果不会被用到</p>
<p>•代码只会影响死变量（只写不读）</p>
<p>下面红框标示的代码就属于死码，满足以上特征</p>
<p><img data-src="https://cdn.jsdelivr.net/gh/huxingyi1997/my_img/img/20210420164837.webp" alt="img"></p>
<p>传统编译型的语言中，都是由编译器将Dead Code从AST（抽象语法树）中删除，那javascript中是由谁做DCE呢？</p>
<p>首先肯定不是浏览器做DCE，因为当我们的代码送到浏览器，那还谈什么消除无法执行的代码来优化呢，所以肯定是送到浏览器之前的步骤进行优化。</p>
<p>其实也不是上面提到的三个工具，rollup，webpack，cc做的，而是著名的代码压缩优化工具uglify，uglify完成了javascript的DCE，下面通过一个实验来验证一下。</p>
<blockquote>
<p>以下所有的示例代码都能在github中找到<a target="_blank" rel="noopener" href="https://github.com/lin-xi/treeshaking/tree/master/rollup-webpack">github.com/lin-xi/tree…</a></p>
</blockquote>
<p><strong>分别用rollup和webpack将图4中的代码进行打包</strong></p>
<p><img data-src="https://cdn.jsdelivr.net/gh/huxingyi1997/my_img/img/20210420164856.webp" alt="img"></p>
<p>中间是rollup打包的结果，右边是webpack打包的结果</p>
<p>可以发现，rollup将无用的代码foo函数和unused函数消除了，但是仍然保留了不会执行到的代码，而webpack完整的保留了所有的无用代码和不会执行到的代码。</p>
<p><strong>分别用rollup + uglify和 webpack + uglify 将图4中的代码进行打包</strong></p>
<p><img data-src="https://cdn.jsdelivr.net/gh/huxingyi1997/my_img/img/20210420164918.webp" alt="img"></p>
<p>图6</p>
<p>中间是配置文件，右侧是结果</p>
<p>可以看到右侧最终打包结果中都去除了无法执行到的代码，结果符合我们的预期。</p>
<p><strong>(2) 再看一下Tree-shaking消除大法</strong></p>
<p>前面提到了tree-shaking更关注于无用模块的消除，消除那些引用了但并没有被使用的模块。</p>
<p>先思考一个问题，为什么tree-shaking是最近几年流行起来了？而前端模块化概念已经有很多年历史了，其实tree-shaking的消除原理是依赖于ES6的模块特性。</p>
<p><img data-src="https://cdn.jsdelivr.net/gh/huxingyi1997/my_img/img/20210420164924.webp" alt="img"></p>
<p>ES6 module 特点：</p>
<ul>
<li>只能作为模块顶层的语句出现</li>
<li>import 的模块名只能是字符串常量</li>
<li>import binding 是 immutable的</li>
</ul>
<p>ES6模块依赖关系是确定的，和运行时的状态无关，可以进行可靠的静态分析，这就是tree-shaking的基础。</p>
<p>所谓静态分析就是不执行代码，从字面量上对代码进行分析，ES6之前的模块化，比如我们可以动态require一个模块，只有执行后才知道引用的什么模块，这个就不能通过静态分析去做优化。</p>
<p>这是 ES6 modules 在设计时的一个重要考量，也是为什么没有直接采用 CommonJS，正是基于这个基础上，才使得 tree-shaking 成为可能，这也是为什么 rollup 和 webpack 2 都要用 ES6 module syntax 才能 tree-shaking。</p>
<p>我们还是通过例子来详细了解一下</p>
<p>面向过程编程函数和面向对象编程是javascript最常用的编程模式和代码组织方式，从这两个方面来实验：</p>
<ul>
<li>函数消除实验</li>
<li>类消除实验</li>
</ul>
<p><strong>先看下函数消除实验</strong></p>
<p>utils中get方法没有被使用到，我们期望的是get方法最终被消除。</p>
<p><img data-src="https://cdn.jsdelivr.net/gh/huxingyi1997/my_img/img/20210420165014.webp" alt="img"></p>
<p>注意，uglify目前不会跨文件去做DCE，所以上面这种情况，uglify是不能优化的。</p>
<p><strong>先看看rollup的打包结果</strong></p>
<p><img data-src="https://cdn.jsdelivr.net/gh/huxingyi1997/my_img/img/20210420165028.webp" alt="img"></p>
<p>完全符合预期，最终结果中没有get方法</p>
<p><strong>再看看webpack的结果</strong></p>
<p><img data-src="https://cdn.jsdelivr.net/gh/huxingyi1997/my_img/img/20210420165134.webp" alt="img"></p>
<p>也符合预期，最终结果中没有get方法</p>
<p>可以看到rollup打包的结果比webpack更优化</p>
<blockquote>
<p>函数消除实验中，rollup和webpack都通过，符合预期</p>
</blockquote>
<p><strong>再来看下类消除实验</strong></p>
<p>增加了对menu.js的引用，但其实代码中并没有用到menu的任何方法和变量，所以我们的期望是，最终代码中menu.js里的内容被消除</p>
<p><img data-src="https://cdn.jsdelivr.net/gh/huxingyi1997/my_img/img/20210420165146.webp" alt="img"></p>
<p>main.js</p>
<p><img data-src="https://cdn.jsdelivr.net/gh/huxingyi1997/my_img/img/20210420165325.webp" alt="img"></p>
<p>menu.js</p>
<p><strong>rollup打包结果</strong></p>
<p><img data-src="https://cdn.jsdelivr.net/gh/huxingyi1997/my_img/img/20210420165433.webp" alt="img"></p>
<p>包中竟然包含了menu.js的全部代码</p>
<p><strong>webpack打包结果</strong></p>
<p><img data-src="https://cdn.jsdelivr.net/gh/huxingyi1997/my_img/img/20210420165505.webp" alt="img"></p>
<p>包中竟然也包含了menu.js的全部代码</p>
<blockquote>
<p>类消除实验中，rollup，webpack 全军覆没，都没有达到预期</p>
</blockquote>
<p>这跟我们想象的完全不一样啊？为什么呢？无用的类不能消除，这还能叫做tree-shaking吗？我当时一度怀疑自己的demo有问题，后来各种网上搜索，才明白demo没有错。</p>
<p>下面摘取了rollup核心贡献者的的一些回答：</p>
<p><img data-src="https://cdn.jsdelivr.net/gh/huxingyi1997/my_img/img/20210420165616.webp"></p>
<p>图7</p>
<ul>
<li>rollup只处理函数和顶层的import/export变量，不能把没用到的类的方法消除掉</li>
<li>javascript动态语言的特性使得静态分析比较困难</li>
<li>图7下部分的代码就是副作用的一个例子，如果静态分析的时候删除里run或者jump，程序运行时就可能报错，那就本末倒置了，我们的目的是优化，肯定不能影响执行</li>
</ul>
<p>再举个例子说明下为什么不能消除menu.js，比如下面这个场景</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">function Menu() &#123;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">Menu.prototype.show &#x3D; function() &#123;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">Array.prototype.unique &#x3D; function() &#123;</span><br><span class="line">    &#x2F;&#x2F; 将 array 中的重复元素去除</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">export default Menu;</span><br></pre></td></tr></table></figure>

<p>如果删除里menu.js，那对Array的扩展也会被删除，就会影响功能。那也许你会问，难道rollup，webpack不能区分是定义Menu的proptotype 还是定义Array的proptotype吗？当然如果代码写成上面这种形式是可以区分的，如果我写成这样呢？</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line">function Menu() &#123;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">Menu.prototype.show &#x3D; function() &#123;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">var a &#x3D; &#39;Arr&#39; + &#39;ay&#39;</span><br><span class="line">var b</span><br><span class="line">if(a &#x3D;&#x3D; &#39;Array&#39;) &#123;</span><br><span class="line">    b &#x3D; Array</span><br><span class="line">&#125; else &#123;</span><br><span class="line">    b &#x3D; Menu</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">b.prototype.unique &#x3D; function() &#123;</span><br><span class="line">    &#x2F;&#x2F; 将 array 中的重复元素去除</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">export default Menu;</span><br></pre></td></tr></table></figure>

<p>这种代码，静态分析是分析不了的，就算能静态分析代码，想要正确完全的分析也比较困难。</p>
<p>更多关于副作用的讨论，可以看这个</p>
<p><img data-src="https://cdn.jsdelivr.net/gh/huxingyi1997/my_img/img/20210420165702.webp" alt="图标"></p>
<p><a href="http://link.zhihu.com/?target=https://github.com/rollup/rollup/issues/349">Tree shaking class methods · Issue #349 · rollup/rollupgithub.com</a></p>
<p><img data-src="https://cdn.jsdelivr.net/gh/huxingyi1997/my_img/img/20210420165709.webp" alt="img"></p>
<blockquote>
<p>tree-shaking对函数效果较好</p>
</blockquote>
<p>函数的副作用相对较少，顶层函数相对来说更容易分析，加上babel默认都是”use strict”严格模式，减少顶层函数的动态访问的方式，也更容易分析</p>
<p>我们开始说的三个工具，rollup和webpack表现不理想，那closure compiler又如何呢？</p>
<p>将示例中的代码用cc打包后得到的结果如下：</p>
<p><img data-src="https://cdn.jsdelivr.net/gh/huxingyi1997/my_img/img/20210420165737.webp" alt="img"></p>
<p>天啊，这不就是我们要的结果吗？完美消除所有无用代码的结果，输出的结果非常性感</p>
<blockquote>
<p>closure compiler， tree-shaking的结果完美！</p>
</blockquote>
<p>可是不能高兴得太早，能得到这么完美结果是需要条件的，那就是cc的侵入式约束规范。必须在代码里添加这样的代码，看红线框标示的</p>
<p><img data-src="https://user-gold-cdn.xitu.io/2018/1/4/160bfd6bf5516b9d?imageView2/0/w/1280/h/960/format/webp/ignore-error/1" alt="img"></p>
<p>google定义一整套注解规范Annotating JavaScript for the Closure Compiler，想更多了解的，可以去看下官网。</p>
<p>侵入式这个就让人很不爽，google Closure Compiler是java写的，和我们基于node的各种构建库不可能兼容（不过目前好像已经有nodejs版 Closure Compiler），Closure Compiler使用起来也比较麻烦，所以虽然效果很赞，但比较难以应用到项目中，迁移成本较大。</p>
<p><strong>说了这么多，总结一下：</strong></p>
<p>三大工具的tree-shaking对于无用代码，无用模块的消除，都是有限的，有条件的。closure compiler是最好的，但与我们日常的基于node的开发流很难兼容。</p>
<p><img data-src="https://user-gold-cdn.xitu.io/2018/1/4/160bfd6c064d09f0?imageView2/0/w/1280/h/960/format/webp/ignore-error/1" alt="img"></p>
<p>tree-shaking对web意义重大，是一个极致优化的理想世界，是前端进化的又一个终极理想。</p>
<p>理想是美好的，但目前还处在发展阶段，还比较困难，有各个方面的，甚至有目前看来无法解</p>
<p>决的问题，但还是应该相信新技术能带来更好的前端世界。</p>
<p>优化是一种态度，不因小而不为，不因艰而不攻。</p>
<h2 id="3-3-tree-shaking实践"><a href="#3-3-tree-shaking实践" class="headerlink" title="3.3 tree-shaking实践"></a>3.3 tree-shaking实践</h2><p><img data-src="https://user-gold-cdn.xitu.io/2018/1/4/160bfde2698a828b?imageView2/0/w/1280/h/960/format/webp/ignore-error/1" alt="img"></p>
<p>webpack2 发布，宣布支持tree-shaking，webpack 3发布，支持作用域提升，生成的bundle文件更小。 再没有升级webpack之前，增幻想我们的性能又要大幅提升了，对升级充满了期待。实际上事实是这样的</p>
<p><img data-src="https://user-gold-cdn.xitu.io/2018/1/4/160bfde269ee60b1?imageView2/0/w/1280/h/960/format/webp/ignore-error/1" alt="img"></p>
<p>升级完之后，bundle文件大小并没有大幅减少，当时有较大的心理落差，然后去研究了为什么效果不理想，原因见 <a target="_blank" rel="noopener" href="https://juejin.im/post/6844903544756109319">Tree-Shaking性能优化实践 - 原理篇</a> 。</p>
<p>优化还是要继续的，虽然工具自带的tree-shaking不能去除太多无用代码，在去除无用代码这一方面也还是有可以做的事情。我们从三个方面做里一些优化。</p>
<h2 id="（1）对组件库引用的优化"><a href="#（1）对组件库引用的优化" class="headerlink" title="（1）对组件库引用的优化"></a>（1）对组件库引用的优化</h2><p>先来看一个问题</p>
<p><img data-src="https://user-gold-cdn.xitu.io/2018/1/4/160bfde26a0b236a?imageView2/0/w/1280/h/960/format/webp/ignore-error/1" alt="img"></p>
<p>当我们使用组件库的时候，import {Button} from ‘element-ui’，相对于Vue.use(elementUI)，已经是具有性能意识，是比较推荐的做法，但如果我们写成右边的形式，具体到文件的引用，打包之后的区别是非常大的，以antd为例，右边形式bundle体积减少约80%。</p>
<p>这个引用也属于有副作用，webpack不能把其他组件进行tree-shaking。既然工具本身是做不了，那我们可以做工具把左边代码自动改成右边代码这种形式。这个工具antd库本身也是提供的。我在antd的工具基础上做了少量的修改，不用任何配置，原生支持我们自己的组件库， <a href="http://link.zhihu.com/?target=https://w-ui.github.io/%23/doc">wui</a> 和 <a href="http://link.zhihu.com/?target=https://wmfe.github.io/xcui/%23/home">xcui</a> 以及一些其他常用的库</p>
<p><strong>babel-plugin-import-fix ，缩小引用范围</strong></p>
<p><img data-src="https://user-gold-cdn.xitu.io/2018/1/4/160bfde26a2f7bcd?imageView2/0/w/1280/h/960/format/webp/ignore-error/1" alt="img"></p>
<p><a href="http://link.zhihu.com/?target=https://github.com/lin-xi/babel-plugin-import-fix"><img data-src="https://user-gold-cdn.xitu.io/2018/1/4/160bfd6bb878d28e?imageView2/0/w/1280/h/960/format/webp/ignore-error/1" alt="图标"></a></p>
<p><a href="http://link.zhihu.com/?target=https://github.com/lin-xi/babel-plugin-import-fix">lin-xi/babel-plugin-import-fix</a></p>
<p>下面介绍一下原理</p>
<p><img data-src="https://user-gold-cdn.xitu.io/2018/1/4/160bfde277e8ea5a?imageView2/0/w/1280/h/960/format/webp/ignore-error/1" alt="img"></p>
<p>这是一个babel的插件，babel通过核心babylon将ES6代码转换成AST抽象语法树，然后插件遍历语法树找出类似import {Button} from ‘element-ui’这样的语句，进行转换，最后重新生成代码。</p>
<p>babel-plugin-import-fix默认支持antd，element，meterial-UI，wui，xcui和d3，只需要再.babelrc中配置插件本身就可以。</p>
<p>.babelrc</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  &quot;presets&quot;: [</span><br><span class="line">    [&quot;es2015&quot;, &#123; &quot;modules&quot;: false &#125;], &quot;react&quot;</span><br><span class="line">  ],</span><br><span class="line">  &quot;plugins&quot;: [&quot;import-fix&quot;]</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p><img data-src="https://user-gold-cdn.xitu.io/2018/1/4/160bfde276aaf2ac?imageView2/0/w/1280/h/960/format/webp/ignore-error/1" alt="img"></p>
<p>其实是想把所有常用的库都默认支持，但很多常用的库却不支持缩小引用范围。因为没有独立输出各个子模块，不能把引用修改为对单个子模块的引用。</p>
<h2 id="（2）CSS-Tree-shaking"><a href="#（2）CSS-Tree-shaking" class="headerlink" title="（2）CSS Tree-shaking"></a>（2）CSS Tree-shaking</h2><p>我们前面所说的tree-shaking都是针对js文件，通过静态分析，尽可能消除无用的代码，那对于css我们能做tree-shaking吗？</p>
<p>随着CSS3，LESS，SASS等各种css预处理语言的普及，css文件在整个工程中占比是不可忽视的。随着大项目功能的不停迭代，导致css中可能就存在着无用的代码。我实现了一个webpack插件来解决这个问题，找出css代码无用的代码。</p>
<p><strong><a href="http://link.zhihu.com/?target=https://github.com/lin-xi/webpack-css-treeshaking-plugin">webpack-css-treeshaking-plugin</a>，对css进行tree-shaking</strong></p>
<p><a href="http://link.zhihu.com/?target=https://github.com/lin-xi/babel-plugin-import-fix"><img data-src="https://user-gold-cdn.xitu.io/2018/1/4/160bfd6bb878d28e?imageView2/0/w/1280/h/960/format/webp/ignore-error/1" alt="图标"></a><br><a href="http://link.zhihu.com/?target=https://github.com/lin-xi/webpack-css-treeshaking-plugin">webpack-css-treeshaking-plugin</a></p>
<p>下面介绍一下原理</p>
<p>整体思路是这样的，遍历所有的css文件中的selector选择器，然后去所有js代码中匹配，如果选择器没有在代码出现过，则认为该选择器是无用代码。</p>
<p><strong>首先面临的问题是，如何优雅的遍历所有的选择器呢？难道要用正则表达式很苦逼的去匹配分割吗？</strong></p>
<p>babel是js世界的福星，其实css世界也有利器，那就是postCss。</p>
<p>PostCSS 提供了一个解析器，它能够将 CSS 解析成AST抽象语法树。然后我们能写各种插件，对抽象语法树做处理，最终生成新的css文件，以达到对css进行精确修改的目的。</p>
<p><img data-src="https://user-gold-cdn.xitu.io/2018/1/4/160bfde28789c23f?imageView2/0/w/1280/h/960/format/webp/ignore-error/1" alt="img"></p>
<p>整体又是一个webpack的插件，架构图如下：</p>
<p><img data-src="https://user-gold-cdn.xitu.io/2018/1/4/160bfde288369a85?imageView2/0/w/1280/h/960/format/webp/ignore-error/1" alt="img"></p>
<p>主要流程：</p>
<ul>
<li>插件监听webapck编译完成事件，webpack编译完成之后，从compilation中找出所有的css文件和js文件</li>
</ul>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line">apply (compiler) &#123;</span><br><span class="line">    compiler.plugin(&#39;after-emit&#39;, (compilation, callback) &#x3D;&gt; &#123;</span><br><span class="line"></span><br><span class="line">      let styleFiles &#x3D; Object.keys(compilation.assets).filter(asset &#x3D;&gt; &#123;</span><br><span class="line">        return &#x2F;\.css$&#x2F;.test(asset)</span><br><span class="line">      &#125;)</span><br><span class="line"></span><br><span class="line">      let jsFiles &#x3D; Object.keys(compilation.assets).filter(asset &#x3D;&gt; &#123;</span><br><span class="line">        return &#x2F;\.(js|jsx)$&#x2F;.test(asset)</span><br><span class="line">      &#125;)</span><br><span class="line"></span><br><span class="line">     ....</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<ul>
<li>将所有的css文件送至postCss处理，找出无用代码</li>
</ul>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line">let tasks &#x3D; []</span><br><span class="line"> styleFiles.forEach((filename) &#x3D;&gt; &#123;</span><br><span class="line">     const source &#x3D; compilation.assets[filename].source()</span><br><span class="line">     let listOpts &#x3D; &#123;</span><br><span class="line">       include: &#39;&#39;,</span><br><span class="line">       source: jsContents,  &#x2F;&#x2F;传入全部js文件</span><br><span class="line">       opts: this.options   &#x2F;&#x2F;插件配置选项</span><br><span class="line">     &#125;</span><br><span class="line">     tasks.push(postcss(treeShakingPlugin(listOpts)).process(source).then(result &#x3D;&gt; &#123;       </span><br><span class="line">       let css &#x3D; result.toString()  &#x2F;&#x2F; postCss处理后的css AST  </span><br><span class="line">       &#x2F;&#x2F;替换webpack的编译产物compilation</span><br><span class="line">       compilation.assets[filename] &#x3D; &#123;</span><br><span class="line">         source: () &#x3D;&gt; css,</span><br><span class="line">         size: () &#x3D;&gt; css.length</span><br><span class="line">       &#125;</span><br><span class="line">       return result</span><br><span class="line">     &#125;))</span><br><span class="line"> &#125;)</span><br></pre></td></tr></table></figure>

<ul>
<li>postCss 遍历，匹配，删除过程</li>
</ul>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br></pre></td><td class="code"><pre><span class="line">module.exports &#x3D; postcss.plugin(&#39;list-selectors&#39;, function (options) &#123;</span><br><span class="line">   &#x2F;&#x2F; 从根节点开始遍历</span><br><span class="line">   cssRoot.walkRules(function (rule) &#123;</span><br><span class="line">     &#x2F;&#x2F; Ignore keyframes, which can log e.g. 10%, 20% as selectors</span><br><span class="line">     if (rule.parent.type &#x3D;&#x3D;&#x3D; &#39;atrule&#39; &amp;&amp; &#x2F;keyframes&#x2F;.test(rule.parent.name)) return</span><br><span class="line">     </span><br><span class="line">     &#x2F;&#x2F; 对每一个规则进行处理</span><br><span class="line">     checkRule(rule).then(result &#x3D;&gt; &#123;</span><br><span class="line">       if (result.selectors.length &#x3D;&#x3D;&#x3D; 0) &#123;</span><br><span class="line">         &#x2F;&#x2F; 选择器全部被删除</span><br><span class="line">         let log &#x3D; &#39; ✂️ [&#39; + rule.selector + &#39;] shaked, [1]&#39;</span><br><span class="line">         console.log(log)</span><br><span class="line">         if (config.remove) &#123;</span><br><span class="line">           rule.remove()</span><br><span class="line">         &#125;</span><br><span class="line">       &#125; else &#123;</span><br><span class="line">         &#x2F;&#x2F; 选择器被部分删除</span><br><span class="line">         let shaked &#x3D; rule.selectors.filter(item &#x3D;&gt; &#123;</span><br><span class="line">           return result.selectors.indexOf(item) &#x3D;&#x3D;&#x3D; -1</span><br><span class="line">         &#125;)</span><br><span class="line">         if (shaked &amp;&amp; shaked.length &gt; 0) &#123;</span><br><span class="line">           let log &#x3D; &#39; ✂️ [&#39; + shaked.join(&#39; &#39;) + &#39;] shaked, [2]&#39;</span><br><span class="line">           console.log(log)</span><br><span class="line">         &#125;</span><br><span class="line">         if (config.remove) &#123;</span><br><span class="line">           &#x2F;&#x2F; 修改AST抽象语法树</span><br><span class="line">           rule.selectors &#x3D; result.selectors</span><br><span class="line">         &#125;</span><br><span class="line">       &#125;</span><br><span class="line">     &#125;)</span><br><span class="line">   &#125;)</span><br></pre></td></tr></table></figure>

<p>checkRule 处理每一个规则核心代码</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br></pre></td><td class="code"><pre><span class="line">let checkRule &#x3D; (rule) &#x3D;&gt; &#123;</span><br><span class="line">      return new Promise(resolve &#x3D;&gt; &#123;</span><br><span class="line">        ...</span><br><span class="line">        let secs &#x3D; rule.selectors.filter(function (selector) &#123;</span><br><span class="line">          let result &#x3D; true</span><br><span class="line">          let processor &#x3D; parser(function (selectors) &#123;</span><br><span class="line">            for (let i &#x3D; 0, len &#x3D; selectors.nodes.length; i &lt; len; i++) &#123;</span><br><span class="line">              let node &#x3D; selectors.nodes[i]</span><br><span class="line">              if (_.includes([&#39;comment&#39;, &#39;combinator&#39;, &#39;pseudo&#39;], node.type)) continue</span><br><span class="line">              for (let j &#x3D; 0, len2 &#x3D; node.nodes.length; j &lt; len2; j++) &#123;</span><br><span class="line">                let n &#x3D; node.nodes[j]</span><br><span class="line">                if (!notCache[n.value]) &#123;</span><br><span class="line">                  switch (n.type) &#123;</span><br><span class="line">                    case &#39;tag&#39;:</span><br><span class="line">                      &#x2F;&#x2F; nothing</span><br><span class="line">                      break</span><br><span class="line">                    case &#39;id&#39;:</span><br><span class="line">                    case &#39;class&#39;:</span><br><span class="line">                      if (!classInJs(n.value)) &#123;</span><br><span class="line">                        &#x2F;&#x2F; 调用classInJs判断是否在JS中出现过</span><br><span class="line">                        notCache[n.value] &#x3D; true</span><br><span class="line">                        result &#x3D; false</span><br><span class="line">                        break</span><br><span class="line">                      &#125;</span><br><span class="line">                      break</span><br><span class="line">                    default:</span><br><span class="line">                      &#x2F;&#x2F; nothing</span><br><span class="line">                      break</span><br><span class="line">                  &#125;</span><br><span class="line">                &#125; else &#123;</span><br><span class="line">                  result &#x3D; false</span><br><span class="line">                  break</span><br><span class="line">                &#125;</span><br><span class="line">              &#125;</span><br><span class="line">            &#125;</span><br><span class="line">          &#125;)</span><br><span class="line">          ...</span><br><span class="line">        &#125;)</span><br><span class="line">        ...</span><br><span class="line">      &#125;)</span><br><span class="line">    &#125;</span><br></pre></td></tr></table></figure>

<p>可以看到其实我只处理里 id选择器和class选择器，id和class相对来说副作用小，引起样式异常的可能性相对较小。</p>
<p>判断css是否再js中出现过，是使用正则匹配。</p>
<p>其实，后续还可以继续优化，比如对tag类的选择器，可以配置是否再html，jsx，template中出现过，如果出现过，没有出现过也可以认为是无用代码。</p>
<p>当然，插件能正常工作还是的有一些前提和约束。我们可以在代码中动态改变css，比如再react和vue中，可以这么写</p>
<p><img data-src="https://user-gold-cdn.xitu.io/2018/1/4/160bfde28dd70e3b?imageView2/0/w/1280/h/960/format/webp/ignore-error/1" alt="img"></p>
<p>这样是比较推荐的方式，选择器作为字符或变量名出现在代码中，下面这样动态生成选择器的情况就会导致匹配失败</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">render()&#123;</span><br><span class="line">  this.stateClass &#x3D; &#39;state-&#39; + this.state &#x3D;&#x3D; 2 ? &#39;open&#39; : &#39;close&#39;</span><br><span class="line">  return &lt;div class&#x3D;&#123;this.stateClass&#125;&gt;&lt;&#x2F;div&gt;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>其中这样情况很容易避免</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">render()&#123;</span><br><span class="line">  this.stateClass &#x3D; this.state &#x3D;&#x3D; 2 ? &#39;state-open&#39; : &#39;state-close&#39;</span><br><span class="line">  return &lt;div class&#x3D;&#123;this.stateClass&#125;&gt;&lt;&#x2F;div&gt;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>所以有一个好的编码规范的约束，插件能更好的工作。</p>
<h2 id="（3）webpack-bundle文件去重"><a href="#（3）webpack-bundle文件去重" class="headerlink" title="（3）webpack bundle文件去重"></a>（3）webpack bundle文件去重</h2><p>如果webpack打包后的bundle文件中存在着相同的模块，也属于无用代码的一种。也应该被去除掉</p>
<p>首先我们需要一个能对bundle文件定性分析的工具，能发现问题，能看出优化效果。</p>
<p>webpack-bundle-analyzer这个插件完全能满足我们的需求，他能以图形化的方式展示bundle中所有的模块的构成的各构成的大小。</p>
<p><img data-src="https://user-gold-cdn.xitu.io/2018/1/4/160bfde29710f82d?imageView2/0/w/1280/h/960/format/webp/ignore-error/1" alt="img"></p>
<p>其次，需求对通用模块进行提取，CommonsChunkPlugin是最被人熟知的用于提供通用模块的插件。早期的时候，我并不完全了解他的功能，并没有发挥最大的功效。</p>
<p>下面介绍CommonsChunkPlugin的正确用法</p>
<p><strong>自动提取所有的node_moudles或者引用次数两次以上的模块</strong></p>
<p><img data-src="https://user-gold-cdn.xitu.io/2018/1/4/160bfde2a12a6464?imageView2/0/w/1280/h/960/format/webp/ignore-error/1" alt="img"></p>
<p>minChunks可以接受一个数值或者函数，如果是函数，可自定义打包规则</p>
<p>但使用上面记载的配置之后，并不能高枕无忧。因为这个配置只能提取所有entry打包后的文件中的通用模块。而现实是，有了提高性能，我们会按需加载，通过webpack提供的import（…）方法，这种按需加载的文件并不会存在于entry之中，所以按需加载的异步模块中的通用模块并没有提取。</p>
<p><strong>如何提取按需加载的异步模块里的通用模块呢？</strong></p>
<p><img data-src="https://user-gold-cdn.xitu.io/2018/1/4/160bfde2a3cd1f44?imageView2/0/w/1280/h/960/format/webp/ignore-error/1" alt="img"></p>
<p>配置另一个CommonsChunkPlugin，添加async属性，async可以接受布尔值或字符串。当时字符串时，默认是输出文件的名称。</p>
<p>names是所有异步模块的名称</p>
<p>这里还涉及一个给异步模块命名的知识点。我是这样做的：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">const Edit &#x3D; resolve &#x3D;&gt; &#123; import( &#x2F;* webpackChunkName: &quot;EditPage&quot; *&#x2F; &#39;.&#x2F;pages&#x2F;Edit&#x2F;Edit&#39;).then((mod) &#x3D;&gt; &#123; resolve(mod.default); &#125;) &#125;;</span><br><span class="line">const PublishPage &#x3D; resolve &#x3D;&gt; &#123; import( &#x2F;* webpackChunkName: &quot;Publish&quot; *&#x2F; &#39;.&#x2F;pages&#x2F;Publish&#x2F;Publish&#39;).then((mod) &#x3D;&gt; &#123; resolve(mod); &#125;) &#125;;</span><br><span class="line">const Models &#x3D; resolve &#x3D;&gt; &#123; import( &#x2F;* webpackChunkName: &quot;Models&quot; *&#x2F; &#39;.&#x2F;pages&#x2F;Models&#x2F;Models&#39;).then((mod) &#x3D;&gt; &#123; resolve(mod.default); &#125;) &#125;;</span><br><span class="line">const MediaUpload &#x3D; resolve &#x3D;&gt; &#123; import( &#x2F;* webpackChunkName: &quot;MediaUpload&quot; *&#x2F; &#39;.&#x2F;pages&#x2F;Media&#x2F;MediaUpload&#39;).then((mod) &#x3D;&gt; &#123; resolve(mod); &#125;) &#125;;</span><br><span class="line">const RealTime &#x3D; resolve &#x3D;&gt; &#123; import( &#x2F;* webpackChunkName: &quot;RealTime&quot; *&#x2F; &#39;.&#x2F;pages&#x2F;RealTime&#x2F;RealTime&#39;).then((mod) &#x3D;&gt; &#123; resolve(mod.default); &#125;) &#125;;</span><br></pre></td></tr></table></figure>

<p>没错，在import里添加注释。/* webpackChunkName: “EditPage” */ ，虽然看着不舒服，但是管用。</p>
<p>贴一个项目的优化效果对比图</p>
<p><img data-src="https://user-gold-cdn.xitu.io/2018/1/4/160bfde2aa751805?imageView2/0/w/1280/h/960/format/webp/ignore-error/1" alt="img"></p>
<p>优化效果还是比较明显。</p>
<p><img data-src="https://user-gold-cdn.xitu.io/2018/1/4/160bfde2aa34e682?imageView2/0/w/1280/h/960/format/webp/ignore-error/1" alt="img"></p>
<p>优化前bundle</p>
<p><img data-src="https://user-gold-cdn.xitu.io/2018/1/4/160bfde2ae93d057?imageView2/0/w/1280/h/960/format/webp/ignore-error/1" alt="img"></p>
<p>优化后bundle</p>
<p><strong>最后思考一个问题：</strong></p>
<p><strong>不同entry模块或按需加载的异步模块需不需要提取通用模块？</strong></p>
<p>这个需要看场景了，比如模块都是在线加载的，如果通用模块提取粒度过小，会导致首页首屏需要的文件变多，很多可能是首屏用不到的，导致首屏过慢，二级或三级页面加载会大幅提升。所以这个就需要根据业务场景做权衡，控制通用模块提取的粒度。</p>
<p>百度外卖的移动端应用场景是这样的，我们所有的移动端页面都做了离线化的处理。离线之后，加载本地的js文件，与网络无关，基本上可以忽略文件大小，所以更关注整个离线包的大小。离线包越小，耗费用户的流量就越小，用户体验更好，所以离线化的场景是非常适合最小粒提取通用模块的，即将所有entry模块和异步加载模块的引用大于2的模块都提取，这样能获得最小的输出文件，最小的离线包。</p>
<h1 id="4-uglify原理"><a href="#4-uglify原理" class="headerlink" title="4.uglify原理"></a>4.uglify原理</h1><h2 id="4-1-AST（抽象语法树）"><a href="#4-1-AST（抽象语法树）" class="headerlink" title="4.1  AST（抽象语法树）"></a>4.1  AST（抽象语法树）</h2><p>要想了解JS的压缩原理，需要首先了解AST。</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">抽象语法树：AST（Abstract Syntax Tree)，是源代码的抽象语法结构的树状表现形式，这里特指编程语言的源代码。树上的每个节点都表示源代码中的一种结构。之所以说语法是「抽象」的，是因为这里的语法并不会表示出真实语法中出现的每个细节。</span><br></pre></td></tr></table></figure>

<p>举个例子：</p>
<p><img data-src="https://www.h5w3.com/wp-content/uploads/2020/06/bVbHPdq.png" alt="image.png"></p>
<p><img data-src="https://www.h5w3.com/wp-content/uploads/2020/06/bVbHPdr.png" alt="image.png"></p>
<p>从上面两个例子中，可以看出AST是源代码根据其语法结构，省略一些细节（比如：括号没有生成节点），抽象成树形表达。抽象语法树在计算机科学中有很多应用，比如编译器、IDE、压缩代码、格式化代码等。[1]</p>
<h2 id="4-2-代码压缩原理"><a href="#4-2-代码压缩原理" class="headerlink" title="4.2 代码压缩原理"></a>4.2 代码压缩原理</h2><p>了解了AST之后，我们再分析一下JS的代码压缩原理。简单的说，就是</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">1. 将code转换成AST</span><br><span class="line">2. 将AST进行优化，生成一个更小的AST</span><br><span class="line">3. 将新生成的AST再转化成code</span><br></pre></td></tr></table></figure>

<p>PS：具体的AST树大家可以在astexplorer上在线获得</p>
<p>babel，eslint，v8的逻辑均与此类似，下图是我们引用了babel的转化示意图：<br><img data-src="https://www.h5w3.com/wp-content/uploads/2020/06/bVbDNYp.jpg" alt="1.jpg"></p>
<p>以我们之前被质疑的代码为例，看看它在uglify中是怎么样一步一步被压缩的：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line">&#x2F;&#x2F; uglify-js的版本需要为2.x, 3.0之后uglifyjs不再暴露Compressor api</span><br><span class="line">&#x2F;&#x2F; 2.x的uglify不能自动解析es6，所以这里先切换成es5</span><br><span class="line">&#x2F;&#x2F; npm install uglify-js@2.x</span><br><span class="line">var UglifyJS &#x3D; require(&#39;uglify-js&#39;);</span><br><span class="line">&#x2F;&#x2F; 原始代码</span><br><span class="line">var code &#x3D; &#96;var a;</span><br><span class="line">var x &#x3D; &#123; b: 123 &#125;;</span><br><span class="line">a &#x3D; 123,</span><br><span class="line">delete x&#96;;</span><br><span class="line">&#x2F;&#x2F; 通过 UglifyJS 把代码解析为 AST</span><br><span class="line">var ast &#x3D; UglifyJS.parse(code);</span><br><span class="line">ast.figure_out_scope();</span><br><span class="line">&#x2F;&#x2F; 转化为一颗更小的 AST 树</span><br><span class="line">compressor &#x3D; UglifyJS.Compressor();</span><br><span class="line">ast &#x3D; ast.transform(compressor);</span><br><span class="line">&#x2F;&#x2F; 再把 AST 转化为代码</span><br><span class="line">code &#x3D; ast.print_to_string();</span><br><span class="line">&#x2F;&#x2F; var a,x&#x3D;&#123;b:123&#125;;a&#x3D;123,delete x;</span><br><span class="line">console.log(&quot;code&quot;, code);</span><br></pre></td></tr></table></figure>

<p>到这里，我们已经了解了uglifyjs的代码压缩原理，但是还没有解决一个问题——为什么某些语句间的分号会被转换为逗号，某些不会转换。这就涉及到了uglifyjs的压缩规则。</p>
<h2 id="4-3-代码压缩规则"><a href="#4-3-代码压缩规则" class="headerlink" title="4.3 代码压缩规则"></a>4.3 代码压缩规则</h2><p>由于uglifyjs的代码压缩规则很多，我们这里只分析与本文中相关的部分:</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">uglifyjs的全部压缩规则可以参见：《[解读uglifyJS（四）——Javascript代码压缩](https:&#x2F;&#x2F;rapheal.sinaapp.com&#x2F;2014&#x2F;05&#x2F;22&#x2F;uglifyjs-squeeze&#x2F;#more-705)》</span><br></pre></td></tr></table></figure>

<p><img data-src="https://www.h5w3.com/wp-content/uploads/2020/06/bVbHPfO.png" alt="image.png"></p>
<p>PS：在线demo</p>
<p>这其中需要注意的是只有“表达式语句”才能被合并，那么什么是表达式语句呢？</p>
<p><strong>表达式 VS 语句 VS 表达式语句</strong></p>
<p>例如：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">    a; &#x2F;&#x2F;返回a的值</span><br><span class="line">b + 3; &#x2F;&#x2F; 返回b+3的结果</span><br></pre></td></tr></table></figure>

<p>例如：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">    if(x &gt; 0) &#123;</span><br><span class="line">...</span><br><span class="line">&#125;</span><br><span class="line">for(var i &#x3D; 0;i &lt; arr.length; i ++) &#123;</span><br><span class="line">...</span><br><span class="line">&#125;</span><br><span class="line">const a &#x3D; 123;</span><br></pre></td></tr></table></figure>

<p>例如：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">    A();</span><br><span class="line">function() &#123;&#125;();</span><br><span class="line">delete x.b;</span><br><span class="line">b &#x3D; b + 3;</span><br></pre></td></tr></table></figure>

<p>综上所述，因为a = 123 和 delete x都是表达式语句，所以分号被转换为逗号。而var x = {b:123}则因为是声明语句，所以和a=123不会合并，分号不会被转换。但var x = {b:123}和第一行var a又触发了另外一条规则，</p>
<p>所以第一行和第二行会被合并为var a,x={b:123}</p>
<h2 id="4-4-总结"><a href="#4-4-总结" class="headerlink" title="4.4 总结"></a>4.4 总结</h2><p>在本文中，我们讨论了什么是抽象语法树，uglifyjs的压缩原理，以及相应的压缩规则，最终明晰了为什么代码会被压缩成我们得到的样子，希望对大家有所帮助。</p>
<h2 id="参考文献"><a href="#参考文献" class="headerlink" title="参考文献"></a>参考文献</h2><p>[1]《抽象语法树在 JavaScript 中的应用》<br>[2]《javascript 代码是如何被压缩的》<br>[3]《[译]JavaScript中:表达式和语句的区别》<br>[4]《解读uglifyJS（四）——Javascript代码压缩》</p>
<h1 id="5-Babel原理"><a href="#5-Babel原理" class="headerlink" title="5.Babel原理"></a>5.<a target="_blank" rel="noopener" href="https://my.oschina.net/u/4088983/blog/4545928">Babel原理</a></h1><h2 id="5-1-什么是-AST"><a href="#5-1-什么是-AST" class="headerlink" title="5.1 什么是 AST"></a>5.1 什么是 AST</h2><p>抽象语法树（<code>Abstract Syntax Tree</code>）简称 <code>AST</code>，是源代码的抽象语法结构的树状表现形式。<code>webpack</code>、<code>eslint</code> 等很多工具库的核心都是通过抽象语法树这个概念来实现对代码的检查、分析等操作。今天我为大家分享一下 JavaScript 这类解释型语言的抽象语法树的概念</p>
<p>我们常用的浏览器就是通过将 js 代码转化为抽象语法树来进行下一步的分析等其他操作。所以将 js 转化为抽象语法树更利于程序的分析。</p>
<p><img data-src="https://oscimg.oschina.net/oscnet/d60e4800-53b4-466b-8cac-a78d3575d237.jpg" alt="img"></p>
<p>如上图中变量声明语句，转换为 AST 之后就是右图中显示的样式</p>
<p>左图中对应的：</p>
<ul>
<li><code>var</code> 是一个关键字</li>
<li><code>AST</code> 是一个定义者</li>
<li><code>=</code> 是 Equal 等号的叫法有很多形式，在后面我们还会看到</li>
<li><code>is tree</code> 是一个字符串</li>
<li><code>;</code> 就是 Semicoion</li>
</ul>
<p>首先一段代码转换成的抽象语法树是一个对象，该对象会有一个顶级的 type 属性 <code>Program</code>；第二个属性是 <code>body</code> 是一个数组。</p>
<p><code>body</code> 数组中存放的每一项都是一个对象，里面包含了所有的对于该语句的描述信息</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">type:         描述该语句的类型  --&gt; 变量声明的语句</span><br><span class="line">kind:         变量声明的关键字  --&gt; var</span><br><span class="line">declaration:  声明内容的数组，里面每一项也是一个对象</span><br><span class="line">            type: 描述该语句的类型</span><br><span class="line">            id:   描述变量名称的对象</span><br><span class="line">                type: 定义</span><br><span class="line">                name: 变量的名字</span><br><span class="line">            init: 初始化变量值的对象</span><br><span class="line">                type:   类型</span><br><span class="line">                value:  值 &quot;is tree&quot; 不带引号</span><br><span class="line">                row:    &quot;\&quot;is tree&quot;\&quot; 带引号</span><br></pre></td></tr></table></figure>

<h2 id="5-2-词法分析和语法分析"><a href="#5-2-词法分析和语法分析" class="headerlink" title="5.2 词法分析和语法分析"></a>5.2 词法分析和语法分析</h2><p><code>JavaScript</code> 是解释型语言，一般通过 词法分析 -&gt; 语法分析 -&gt; 语法树，就可以开始解释执行了</p>
<p>词法分析：也叫<code>扫描</code>，是将字符流转换为记号流(<code>tokens</code>)，它会读取我们的代码然后按照一定的规则合成一个个的标识</p>
<p>比如说：<code>var a = 2</code> ，这段代码通常会被分解成 <code>var、a、=、2</code></p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">;[</span><br><span class="line">  &#123; type: &#39;Keyword&#39;, value: &#39;var&#39; &#125;,</span><br><span class="line">  &#123; type: &#39;Identifier&#39;, value: &#39;a&#39; &#125;,</span><br><span class="line">  &#123; type: &#39;Punctuator&#39;, value: &#39;&#x3D;&#39; &#125;,</span><br><span class="line">  &#123; type: &#39;Numeric&#39;, value: &#39;2&#39; &#125;,</span><br><span class="line">]</span><br></pre></td></tr></table></figure>

<p>当词法分析源代码的时候，它会一个一个字符的读取代码，所以很形象地称之为扫描 - <code>scans</code>。当它遇到空格、操作符，或者特殊符号的时候，它会认为一个话已经完成了。</p>
<p>语法分析：也称<code>解析器</code>，将词法分析出来的数组转换成树的形式，同时验证语法。语法如果有错的话，抛出语法错误。</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">&#123;  ...  &quot;type&quot;: &quot;VariableDeclarator&quot;,  &quot;id&quot;: &#123;    &quot;type&quot;: &quot;Identifier&quot;,    &quot;name&quot;: &quot;a&quot;  &#125;,  ...&#125;</span><br></pre></td></tr></table></figure>

<p>语法分析成 AST ，我们可以在这里在线看到效果 <a target="_blank" rel="noopener" href="http://esprima.org/">http://esprima.org</a></p>
<h2 id="5-3-AST-能做什么"><a href="#5-3-AST-能做什么" class="headerlink" title="5.3 AST 能做什么"></a>5.3 AST 能做什么</h2><ul>
<li>语法检查、代码风格检查、格式化代码、语法高亮、错误提示、自动补全等</li>
<li>代码混淆压缩</li>
<li>优化变更代码，改变代码结构等</li>
</ul>
<p>比如说，有个函数 <code>function a() &#123;&#125;</code> 我想把它变成 <code>function b() &#123;&#125;</code></p>
<p>比如说，在 <code>webpack</code> 中代码编译完成后 <code>require(&#39;a&#39;) --&gt; __webapck__require__(&quot;*/**/a.js&quot;)</code></p>
<p>下面来介绍一套工具，可以把代码转成语法树然后改变节点以及重新生成代码</p>
<h2 id="5-4-AST-解析流程"><a href="#5-4-AST-解析流程" class="headerlink" title="5.4 AST 解析流程"></a>5.4 AST 解析流程</h2><p>准备工具：</p>
<ul>
<li>esprima：code =&gt; ast 代码转 ast</li>
<li>estraverse: traverse ast 转换树</li>
<li>escodegen: ast =&gt; code</li>
</ul>
<p>在推荐一个常用的 AST 在线转换网站：<a target="_blank" rel="noopener" href="https://astexplorer.net/">https://astexplorer.net/</a></p>
<p>比如说一段代码 <code>function getUser() &#123;&#125;</code>，我们把函数名字更改为 <code>hello</code>，看代码流程</p>
<p>看以下代码，简单说明 <code>AST</code> 遍历流程</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line">const esprima &#x3D; require(&#39;esprima&#39;)</span><br><span class="line">const estraverse &#x3D; require(&#39;estraverse&#39;)</span><br><span class="line">const code &#x3D; &#96;function getUser() &#123;&#125;&#96;</span><br><span class="line">&#x2F;&#x2F; 生成 AST</span><br><span class="line">const ast &#x3D; esprima.parseScript(code)</span><br><span class="line">&#x2F;&#x2F; 转换 AST，只会遍历 type 属性</span><br><span class="line">&#x2F;&#x2F; traverse 方法中有进入和离开两个钩子函数</span><br><span class="line">estraverse.traverse(ast, &#123;</span><br><span class="line">  enter(node) &#123;</span><br><span class="line">    console.log(&#39;enter -&gt; node.type&#39;, node.type)</span><br><span class="line">  &#125;,</span><br><span class="line">  leave(node) &#123;</span><br><span class="line">    console.log(&#39;leave -&gt; node.type&#39;, node.type)</span><br><span class="line">  &#125;,</span><br><span class="line">&#125;)</span><br></pre></td></tr></table></figure>

<p>输出结果如下：</p>
<p><img data-src="https://oscimg.oschina.net/oscnet/f6ecc074-bf06-4d8a-b25b-836cdc32e2c6.jpg" alt="img"></p>
<p>由此可以得到 AST 遍历的流程是深度优先，遍历过程如下：</p>
<p><img data-src="https://oscimg.oschina.net/oscnet/c7408a95-6cfb-43c0-9f48-e49634e254df.jpg" alt="img"></p>
<h2 id="5-5-修改函数名字"><a href="#5-5-修改函数名字" class="headerlink" title="5.5 修改函数名字"></a>5.5 修改函数名字</h2><p>此时我们发现函数的名字在 <code>type</code> 为 <code>Identifier</code> 的时候就是该函数的名字，我们就可以直接修改它便可实现一个更改函数名字的 <code>AST</code> 工具</p>
<p><img data-src="https://oscimg.oschina.net/oscnet/948eebb7-a184-4ba4-a82b-d500587d39a9.jpg" alt="img"></p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line">&#x2F;&#x2F; 转换树</span><br><span class="line">estraverse.traverse(ast, &#123;</span><br><span class="line">  &#x2F;&#x2F; 进入离开修改都是可以的</span><br><span class="line">  enter(node) &#123;</span><br><span class="line">    console.log(&#39;enter -&gt; node.type&#39;, node.type)</span><br><span class="line">    if (node.type &#x3D;&#x3D;&#x3D; &#39;Identifier&#39;) &#123;</span><br><span class="line">      node.name &#x3D; &#39;hello&#39;</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;,</span><br><span class="line">  leave(node) &#123;</span><br><span class="line">    console.log(&#39;leave -&gt; node.type&#39;, node.type)</span><br><span class="line">  &#125;,</span><br><span class="line">&#125;)</span><br><span class="line">&#x2F;&#x2F; 生成新的代码</span><br><span class="line">const result &#x3D; escodegen.generate(ast)</span><br><span class="line">console.log(result)</span><br><span class="line">&#x2F;&#x2F; function hello() &#123;&#125;</span><br></pre></td></tr></table></figure>

<h2 id="5-6-babel-工作原理"><a href="#5-6-babel-工作原理" class="headerlink" title="5.6 babel 工作原理"></a>5.6 babel 工作原理</h2><p>提到 AST 我们肯定会想到 babel，自从 Es6 开始大规模使用以来，babel 就出现了，它主要解决了就是一些浏览器不兼容 Es6 新特性的问题，其实就把 Es6 代码转换为 Es5 的代码，兼容所有浏览器，babel 转换代码其实就是用了 AST，babel 与 AST 就有着很一种特别的关系。</p>
<p>那么我们就在 babel 的中来使用 AST，看看 babel 是如何编译代码的（不讲源码啊）</p>
<p>需要用到两个工具包 <code>@babel/core</code>、<code>@babel/preset-env</code></p>
<p>当我们配置 babel 的时候，不管是在 <code>.babelrc</code> 或者 <code>babel.config.js</code> 文件里面配置的都有 <code>presets</code> 和 <code>plugins</code> 两个配置项（还有其他配置项，这里不做介绍）</p>
<h3 id="5-6-1-插件和预设的区别"><a href="#5-6-1-插件和预设的区别" class="headerlink" title="5.6.1 插件和预设的区别"></a>5.6.1 插件和预设的区别</h3><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">&#x2F;&#x2F; .babelrc</span><br><span class="line">&#123;</span><br><span class="line">  &quot;presets&quot;: [&quot;@babel&#x2F;preset-env&quot;],</span><br><span class="line">  &quot;plugins&quot;: []</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>当我们配置了 <code>presets</code> 中有 <code>@babel/preset-env</code>，那么 <code>@babel/core</code> 就会去找 <code>preset-env</code> 预设的插件包，它是一套</p>
<p>babel 核心包并不会去转换代码，核心包只提供一些核心 API，真正的代码转换工作由插件或者预设来完成，比如要转换箭头函数，会用到这个 plugin，<code>@babel/plugin-transform-arrow-functions</code>，当需要转换的要求增加时，我们不可能去一一配置相应的 plugin，这个时候就可以用到预设了，也就是 presets。presets 是 plugins 的集合，一个 presets 内部包含了很多 plugin。</p>
<h3 id="5-6-2-babel-插件的使用"><a href="#5-6-2-babel-插件的使用" class="headerlink" title="5.6.2 babel 插件的使用"></a>5.6.2 babel 插件的使用</h3><p>现在我们有一个箭头函数，要想把它转成普通函数，我们就可以直接这么写：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line">const babel &#x3D; require(&#39;@babel&#x2F;core&#39;)</span><br><span class="line">const code &#x3D; &#96;const fn &#x3D; (a, b) &#x3D;&gt; a + b&#96;</span><br><span class="line">&#x2F;&#x2F; babel 有 transform 方法会帮我们自动遍历，使用相应的预设或者插件转换相应的代码</span><br><span class="line">const r &#x3D; babel.transform(code, &#123;</span><br><span class="line">  presets: [&#39;@babel&#x2F;preset-env&#39;],</span><br><span class="line">&#125;)</span><br><span class="line">console.log(r.code)</span><br><span class="line">&#x2F;&#x2F; 打印结果如下</span><br><span class="line">&#x2F;&#x2F; &quot;use strict&quot;;</span><br><span class="line">&#x2F;&#x2F; var fn &#x3D; function fn() &#123; return a + b; &#125;;</span><br></pre></td></tr></table></figure>

<p>此时我们可以看到最终代码会被转成普通函数，但是我们，只需要箭头函数转通函数的功能，不需要用这么大一套包，只需要一个箭头函数转普通函数的包，我们其实是可以在 <code>node_modules</code> 下面找到有个叫做 <code>plugin-transform-arrow-functions</code> 的插件，这个插件是专门用来处理 箭头函数的，我们就可以这么写：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">const r &#x3D; babel.transform(code, &#123;</span><br><span class="line">  plugins: [&#39;@babel&#x2F;plugin-transform-arrow-functions&#39;],</span><br><span class="line">&#125;)</span><br><span class="line">console.log(r.code)</span><br><span class="line">&#x2F;&#x2F; 打印结果如下</span><br><span class="line">&#x2F;&#x2F; const fn &#x3D; function () &#123; return a + b; &#125;;</span><br></pre></td></tr></table></figure>

<p>我们可以从打印结果发现此时并没有转换我们变量的声明方式还是 const 声明，只是转换了箭头函数</p>
<h2 id="5-7-编写自己的插件"><a href="#5-7-编写自己的插件" class="headerlink" title="5.7 编写自己的插件"></a>5.7 编写自己的插件</h2><blockquote>
<p>此时，我们就可以自己来写一些插件，来实现代码的转换，中间处理代码的过程就是使用前面提到的 AST 的处理逻辑</p>
</blockquote>
<p>现在我们来个实战把 <code>const fn = (a, b) =&gt; a + b</code> 转换为 <code>const fn = function(a, b) &#123; return a + b &#125;</code></p>
<h3 id="5-7-1-分析-AST-结构"><a href="#5-7-1-分析-AST-结构" class="headerlink" title="5.7.1 分析 AST 结构"></a>5.7.1 分析 AST 结构</h3><p>首先我们在在线分析 AST 的网站上分析 <code>const fn = (a, b) =&gt; a + b</code> 和 <code>const fn = function(a, b) &#123; return a + b &#125;</code>看两者语法树的区别</p>
<p><img data-src="https://oscimg.oschina.net/oscnet/2730d973-5fbc-4ac2-9169-4922b3c6e4d5.jpg" alt="img"></p>
<p>根据我们分析可得：</p>
<ol>
<li>变成普通函数之后他就不叫箭头函数了 <code>ArrowFunctionExpression</code>，而是函数表达式了 <code>FunctionExpression</code></li>
<li>所以首先我们要把 <code>箭头函数表达式(ArrowFunctionExpression)</code> 转换为 <code>函数表达式(FunctionExpression)</code></li>
<li>要把 <code>二进制表达式(BinaryExpression)</code> 放到一个 <code>代码块中(BlockStatement)</code></li>
<li>其实我们要做就是把一棵树变成另外一颗树，说白了其实就是拼成另一颗树的结构，然后生成新的代码，就可以完成代码的转换</li>
</ol>
<h3 id="5-7-2-访问者模式"><a href="#5-7-2-访问者模式" class="headerlink" title="5.7.2 访问者模式"></a>5.7.2 访问者模式</h3><p>在 babel 中，我们开发 plugins 的时候要用到访问者模式，就是说在访问到某一个路径的时候进行匹配，然后在对这个节点进行修改，比如说上面的当我们访问到 <code>ArrowFunctionExpression</code> 的时候，对 <code>ArrowFunctionExpression</code> 进行修改，变成普通函数</p>
<p>那么我们就可以这么写：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line">const babel &#x3D; require(&#39;@babel&#x2F;core&#39;)</span><br><span class="line">const code &#x3D; &#96;const fn &#x3D; (a, b) &#x3D;&gt; a + b&#96; &#x2F;&#x2F; 转换后 const fn &#x3D; function(a, b) &#123; return a + b &#125;</span><br><span class="line">const arrowFnPlugin &#x3D; &#123;</span><br><span class="line">  &#x2F;&#x2F; 访问者模式</span><br><span class="line">  visitor: &#123;</span><br><span class="line">    &#x2F;&#x2F; 当访问到某个路径的时候进行匹配</span><br><span class="line">    ArrowFunctionExpression(path) &#123;</span><br><span class="line">      &#x2F;&#x2F; 拿到节点</span><br><span class="line">      const node &#x3D; path.node</span><br><span class="line">      console.log(&#39;ArrowFunctionExpression -&gt; node&#39;, node)</span><br><span class="line">    &#125;,</span><br><span class="line">  &#125;,</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">const r &#x3D; babel.transform(code, &#123;</span><br><span class="line">  plugins: [arrowFnPlugin],</span><br><span class="line">&#125;)</span><br><span class="line"></span><br><span class="line">console.log(r)</span><br></pre></td></tr></table></figure>

<h3 id="5-7-3-修改-AST-结构"><a href="#5-7-3-修改-AST-结构" class="headerlink" title="5.7.3 修改 AST 结构"></a>5.7.3 修改 AST 结构</h3><p>此时我们拿到的结果是这样的节点结果是 这样的，其实就是 <code>ArrowFunctionExpression</code> 的 AST，此时我们要做的是把 <code>ArrowFunctionExpression</code> 的结构替换成 <code>FunctionExpression</code>的结构，但是需要我们组装类似的结构，这么直接写很麻烦，但是 babel 为我们提供了一个工具叫做 <code>@babel/types</code></p>
<p><code>@babel/types</code> 有两个作用：</p>
<ol>
<li>判断这个节点是不是这个节点（ArrowFunctionExpression 下面的 path.node 是不是一个 ArrowFunctionExpression）</li>
<li>生成对应的表达式</li>
</ol>
<p>然后我们使用的时候，需要经常查文档，因为里面的节点类型特别多，不是做编译相关工作的是记不住怎么多节点的</p>
<p>那么接下来我们就开始生成一个 <code>FunctionExpression</code>，然后把之前的 <code>ArrowFunctionExpression</code> 替换掉，我们可以看 <code>types</code> 文档，找到 <code>functionExpression</code>，该方法接受相应的参数我们传递过去即可生成一个 <code>FunctionExpression</code></p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">t.functionExpression(id, params, body, generator, async)</span><br></pre></td></tr></table></figure>

<ul>
<li>id: Identifier (default: null) id 可传递 null</li>
<li>params: Array<LVal> (required) 函数参数，可以把之前的参数拿过来</li>
<li>body: BlockStatement (required) 函数体，接受一个 <code>BlockStatement</code> 我们需要生成一个</li>
<li>generator: boolean (default: false) 是否为 generator 函数，当然不是了</li>
<li>async: boolean (default: false) 是否为 async 函数，肯定不是了</li>
</ul>
<p>还需要生成一个 <code>BlockStatement</code>，我们接着看文档找到 <code>BlockStatement</code> 接受的参数</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">t.blockStatement(body, directives)</span><br></pre></td></tr></table></figure>

<p>看文档说明，blockStatement 接受一个 body，那我们把之前的 body 拿过来就可以直接用，不过这里 body 接受一个数组</p>
<p>我们细看 AST 结构，函数表达式中的 <code>BlockStatement</code> 中的 <code>body</code> 是一个 <code>ReturnStatement</code>，所以我们还需要生成一个 <code>ReturnStatement</code></p>
<p>现在我们就可以改写 AST 了</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><span class="line">const babel &#x3D; require(&#39;@babel&#x2F;core&#39;)</span><br><span class="line">const t &#x3D; require(&#39;@babel&#x2F;types&#39;)</span><br><span class="line">const code &#x3D; &#96;const fn &#x3D; (a, b) &#x3D;&gt; a + b&#96; &#x2F;&#x2F; const fn &#x3D; function(a, b) &#123; return a + b &#125;</span><br><span class="line">const arrowFnPlugin &#x3D; &#123;</span><br><span class="line">  &#x2F;&#x2F; 访问者模式</span><br><span class="line">  visitor: &#123;</span><br><span class="line">    &#x2F;&#x2F; 当访问到某个路径的时候进行匹配</span><br><span class="line">    ArrowFunctionExpression(path) &#123;</span><br><span class="line">      &#x2F;&#x2F; 拿到节点然后替换节点</span><br><span class="line">      const node &#x3D; path.node</span><br><span class="line">      console.log(&#39;ArrowFunctionExpression -&gt; node&#39;, node)</span><br><span class="line">      &#x2F;&#x2F; 拿到函数的参数</span><br><span class="line">      const params &#x3D; node.params</span><br><span class="line">      const body &#x3D; node.body</span><br><span class="line">      const functionExpression &#x3D; t.functionExpression(null, params, t.blockStatement([body]))</span><br><span class="line">      &#x2F;&#x2F; 替换原来的函数</span><br><span class="line">      path.replaceWith(functionExpression)</span><br><span class="line">    &#125;,</span><br><span class="line">  &#125;,</span><br><span class="line">&#125;</span><br><span class="line">const r &#x3D; babel.transform(code, &#123;</span><br><span class="line">  plugins: [arrowFnPlugin],</span><br><span class="line">&#125;)</span><br><span class="line">console.log(r.code) &#x2F;&#x2F; const fn &#x3D; function (a, b) &#123; return a + b; &#125;;</span><br></pre></td></tr></table></figure>

<h3 id="5-7-4-特殊情况"><a href="#5-7-4-特殊情况" class="headerlink" title="5.7.4 特殊情况"></a>5.7.4 特殊情况</h3><p>我们知道在剪头函数中是可以省略 <code>return</code> 关键字，我们上面是处理了省略关键字的写法，但是如果用户写了 return 关键字后，我们写的这个插件就有问题了，所以我们可以在优化一下</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">const fn &#x3D; (a, b) &#x3D;&gt; &#123; retrun a + b &#125;&#96; -&gt; &#96;const fn &#x3D; function(a, b) &#123; return a + b &#125;</span><br></pre></td></tr></table></figure>

<p>观察代码我们发现，我们就不需要把 body 转换成 blockStatement 了，直接放过去就可以了，那么我们就可以这么写</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line">ArrowFunctionExpression(path) &#123;</span><br><span class="line">  &#x2F;&#x2F; 拿到节点然后替换节点</span><br><span class="line">  const node &#x3D; path.node</span><br><span class="line">  console.log(&quot;ArrowFunctionExpression -&gt; node&quot;, node)</span><br><span class="line">  &#x2F;&#x2F; 拿到函数的参数</span><br><span class="line">  const params &#x3D; node.params</span><br><span class="line">  let body &#x3D; node.body</span><br><span class="line">  &#x2F;&#x2F; 判断是不是 blockStatement，不是的话让他变成 blockStatement</span><br><span class="line">  if (!t.isBlockStatement(body)) &#123;</span><br><span class="line">    body &#x3D; t.blockStatement([body])</span><br><span class="line">  &#125;</span><br><span class="line">  const functionExpression &#x3D; t.functionExpression(null, params, body)</span><br><span class="line">  &#x2F;&#x2F; 替换原来的函数</span><br><span class="line">  path.replaceWith(functionExpression)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<h2 id="5-8-按需引入"><a href="#5-8-按需引入" class="headerlink" title="5.8 按需引入"></a>5.8 按需引入</h2><p>在开发中，我们引入 UI 框架，比如 vue 中用到的 <code>element-ui</code>，<code>vant</code> 或者 <code>React</code> 中的 <code>antd</code> 都支持全局引入和按需引入，默认是全局引入，如果需要按需引入就需要安装一个 <code>babel-plugin-import</code> 的插件，将全局的写法变成按需引入的写法。</p>
<p>就拿我最近开发移动端用的 vant 为例， <code>import &#123; Button &#125; from &#39;vant&#39;</code> 这种写法经过这个插件之后会变成 <code>import Button from &#39;vant/lib/Button&#39;</code> 这种写法，引用整个 vant 变成了我只用了 vant 下面的某一个文件，打包后的文件会比全部引入的文件大小要小很多</p>
<h3 id="5-8-1-分析语法树"><a href="#5-8-1-分析语法树" class="headerlink" title="5.8.1 分析语法树"></a>5.8.1 分析语法树</h3><blockquote>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">import &#123; Button, Icon &#125; from &#39;vant&#39;&#96; 写法转换为 &#96;import Button from &#39;vant&#x2F;lib&#x2F;Button&#39;; import Icon from &#39;vant&#x2F;lib&#x2F;Icon&#39;</span><br></pre></td></tr></table></figure>
</blockquote>
<p>看一下两个语法树的区别</p>
<p><img data-src="https://oscimg.oschina.net/oscnet/822c18e6-1d5d-406f-ab28-b4dad891071f.jpg" alt="img"></p>
<p>根据两张图分析我们可以得到一些信息：</p>
<ol>
<li>我们发现解构方式引入的模块只有 import 声明，第二张图是两个 import 声明</li>
<li>解构方式引入的详细说明里面( <code>specifiers</code>)是两个 <code>ImportSpecifier</code>，第二张图里面是分开的，而且都是 <code>ImportDefaultSpecifier</code></li>
<li>他们引入的 <code>source</code> 也不一样</li>
<li>那我们要做的其实就是要把单个的 <code>ImportDeclaration</code> 变成多个 <code>ImportDeclaration</code>, 然后把单个 import 解构引入的 <code>specifiers</code> 部分 <code>ImportSpecifier</code> 转换成多个 <code>ImportDefaultSpecifier</code> 并修改对应的 <code>source</code> 即可</li>
</ol>
<h3 id="5-8-2-分析类型"><a href="#5-8-2-分析类型" class="headerlink" title="5.8.2 分析类型"></a>5.8.2 分析类型</h3><p>为了方便传递参数，这次我们写到一个函数里面，可以方便传递转换后拼接的目录</p>
<p>这里我们需要用到的几个类型，也需要在 types 官网上找对应的解释</p>
<ul>
<li><p>首先我们要生成多个 <code>importDeclaration</code> 类型</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">&#x2F;**</span><br><span class="line"> * @param &#123;Array&lt;ImportSpecifier | ImportDefaultSpecifier | ImportNamespaceSpecifier&gt;&#125; specifiers  (required)</span><br><span class="line"> * @param &#123;StringLiteral&#125; source (required)</span><br><span class="line"> *&#x2F;</span><br><span class="line">t.importDeclaration(specifiers, source)</span><br></pre></td></tr></table></figure>
</li>
<li><p>在 <code>importDeclaration</code> 中需要生成 <code>ImportDefaultSpecifier</code></p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">&#x2F;**</span><br><span class="line"> * @param &#123;Identifier&#125; local  (required)</span><br><span class="line"> *&#x2F;</span><br><span class="line">t.importDefaultSpecifier(local)</span><br></pre></td></tr></table></figure>
</li>
<li><p>在 <code>importDeclaration</code> 中还需要生成一个 <code>StringLiteral</code></p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">&#x2F;**</span><br><span class="line"> * @param &#123;string&#125; value  (required)</span><br><span class="line"> *&#x2F;</span><br><span class="line">t.stringLiteral(value)</span><br></pre></td></tr></table></figure>

</li>
</ul>
<h3 id="5-8-3-上代码"><a href="#5-8-3-上代码" class="headerlink" title="5.8.3 上代码"></a>5.8.3 上代码</h3><p>按照上面的分析，我们开始上代码</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br></pre></td><td class="code"><pre><span class="line">const babel &#x3D; require(&#39;@babel&#x2F;core&#39;)</span><br><span class="line">const t &#x3D; require(&#39;@babel&#x2F;types&#39;)</span><br><span class="line">const code &#x3D; &#96;import &#123; Button, Icon &#125; from &#39;vant&#39;&#96;</span><br><span class="line">&#x2F;&#x2F; import Button from &#39;vant&#x2F;lib&#x2F;Button&#39;</span><br><span class="line">&#x2F;&#x2F; import Icon from &#39;vant&#x2F;lib&#x2F;Icon&#39;</span><br><span class="line">function importPlugin(opt) &#123;</span><br><span class="line">  const &#123; libraryDir &#125; &#x3D; opt</span><br><span class="line">  return &#123;</span><br><span class="line">    visitor: &#123;</span><br><span class="line">      ImportDeclaration(path) &#123;</span><br><span class="line">        const node &#x3D; path.node</span><br><span class="line">        &#x2F;&#x2F; console.log(&quot;ImportDeclaration -&gt; node&quot;, node)</span><br><span class="line">        &#x2F;&#x2F; 得到节点的详细说明，然后转换成多个的 import 声明</span><br><span class="line">        const specifiers &#x3D; node.specifiers</span><br><span class="line">        &#x2F;&#x2F; 要处理这个我们做一些判断，首先判断不是默认导出我们才处理，要考虑 import vant, &#123; Button, Icon &#125; from &#39;vant&#39; 写法</span><br><span class="line">        &#x2F;&#x2F; 还要考虑 specifiers 的长度，如果长度不是 1 并且不是默认导出我们才需要转换</span><br><span class="line">        if (!(specifiers.length &#x3D;&#x3D;&#x3D; 1 &amp;&amp; t.isImportDefaultSpecifier(specifiers[0]))) &#123;</span><br><span class="line">          const result &#x3D; specifiers.map((specifier) &#x3D;&gt; &#123;</span><br><span class="line">            const local &#x3D; specifier.local</span><br><span class="line">            const source &#x3D; t.stringLiteral(&#96;$&#123;node.source.value&#125;&#x2F;$&#123;libraryDir&#125;&#x2F;$&#123;specifier.local.name&#125;&#96;)</span><br><span class="line">            &#x2F;&#x2F; console.log(&quot;ImportDeclaration -&gt; specifier&quot;, specifier)</span><br><span class="line">            return t.importDeclaration([t.importDefaultSpecifier(local)],source)</span><br><span class="line">          &#125;)</span><br><span class="line">          console.log(&#39;ImportDeclaration -&gt; result&#39;, result)</span><br><span class="line">          &#x2F;&#x2F; 因为这次要替换的 AST 不是一个，而是多个的，所以需要 &#96;path.replaceWithMultiple(result)&#96; 来替换，但是一执行发现死循环了</span><br><span class="line">          path.replaceWithMultiple(result)</span><br><span class="line">        &#125;</span><br><span class="line">      &#125;,</span><br><span class="line">    &#125;,</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br><span class="line">const r &#x3D; babel.transform(code, &#123;</span><br><span class="line">  plugins: [importPlugin(&#123; libraryDir: &#39;lib&#39; &#125;)],</span><br><span class="line">&#125;)</span><br><span class="line">console.log(r.code)</span><br></pre></td></tr></table></figure>

<p>看打印结果和转换结果似乎没什么问题，这个插件几乎就实现了</p>
<p><img data-src="https://oscimg.oschina.net/oscnet/1fe23797-e87f-4e81-93c2-c420c959e8a0.jpg" alt="img"></p>
<h3 id="5-8-4-特殊情况"><a href="#5-8-4-特殊情况" class="headerlink" title="5.8.4 特殊情况"></a>5.8.4 特殊情况</h3><p>但是我们考虑一种情况，如果用户不全部按需加载了，按需加载只是一种选择，如果用户这么写了 <code>import vant, &#123; Button, Icon &#125; from &#39;vant&#39;</code>，那么我们这个插件就出现问题了</p>
<p><img data-src="https://oscimg.oschina.net/oscnet/3b6def74-c525-4f94-9ebb-deafb72b7e28.jpg" alt="img"></p>
<p>如果遇到这种写法，那么默认导入的他的 <code>source</code> 应该是不变的，我们要把原来的 <code>source</code> 拿出来</p>
<p>所以还需要判断一下，每一个 <code>specifier</code> 是不是一个 <code>ImportDefaultSpecifier</code> 然后处理不同的 <code>source</code>，完整处理逻辑应该如下</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br></pre></td><td class="code"><pre><span class="line">function importPlugin(opt) &#123;</span><br><span class="line">  const &#123; libraryDir &#125; &#x3D; opt</span><br><span class="line">  return &#123;</span><br><span class="line">    visitor: &#123;</span><br><span class="line">      ImportDeclaration(path) &#123;</span><br><span class="line">        const node &#x3D; path.node</span><br><span class="line">        &#x2F;&#x2F; console.log(&quot;ImportDeclaration -&gt; node&quot;, node)</span><br><span class="line">        &#x2F;&#x2F; 得到节点的详细说明，然后转换成多个的 import 声明</span><br><span class="line">        const specifiers &#x3D; node.specifiers</span><br><span class="line">        &#x2F;&#x2F; 要处理这个我们做一些判断，首先判断不是默认导出我们才处理，要考虑 import vant, &#123; Button, Icon &#125; from &#39;vant&#39; 写法</span><br><span class="line">        &#x2F;&#x2F; 还要考虑 specifiers 的长度，如果长度不是 1 并且不是默认导出我们才需要转换</span><br><span class="line">        if (</span><br><span class="line">          !(</span><br><span class="line">            specifiers.length &#x3D;&#x3D;&#x3D; 1 &amp;&amp; t.isImportDefaultSpecifier(specifiers[0])</span><br><span class="line">          )</span><br><span class="line">        ) &#123;</span><br><span class="line">          const result &#x3D; specifiers.map((specifier) &#x3D;&gt; &#123;</span><br><span class="line">            let local &#x3D; specifier.local,</span><br><span class="line">              source</span><br><span class="line">            &#x2F;&#x2F; 判断是否存在默认导出的情况</span><br><span class="line">            if (t.isImportDefaultSpecifier(specifier)) &#123;</span><br><span class="line">              source &#x3D; t.stringLiteral(node.source.value)</span><br><span class="line">            &#125; else &#123;</span><br><span class="line">              source &#x3D; t.stringLiteral(</span><br><span class="line">                &#96;$&#123;node.source.value&#125;&#x2F;$&#123;libraryDir&#125;&#x2F;$&#123;specifier.local.name&#125;&#96;</span><br><span class="line">              )</span><br><span class="line">            &#125;</span><br><span class="line">            return t.importDeclaration(</span><br><span class="line">              [t.importDefaultSpecifier(local)],</span><br><span class="line">              source</span><br><span class="line">            )</span><br><span class="line">          &#125;)</span><br><span class="line">          path.replaceWithMultiple(result)</span><br><span class="line">        &#125;</span><br><span class="line">      &#125;,</span><br><span class="line">    &#125;,</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<h2 id="5-9-babylon"><a href="#5-9-babylon" class="headerlink" title="5.9 babylon"></a>5.9 babylon</h2><blockquote>
<p>在 babel 官网上有一句话 Babylon is a JavaScript parser used in Babel.</p>
</blockquote>
<h3 id="5-9-1-babylon-与-babel-的关系"><a href="#5-9-1-babylon-与-babel-的关系" class="headerlink" title="5.9.1 babylon 与 babel 的关系"></a>5.9.1 babylon 与 babel 的关系</h3><p><code>babel</code> 使用的引擎是 <code>babylon</code>，<code>Babylon</code> 并非 <code>babel</code> 团队自己开发的，而是 fork 的 <code>acorn</code> 项目，<code>acorn</code> 的项目本人在很早之前在兴趣部落 1.0 在构建中使用，为了是做一些代码的转换，是很不错的一款引擎，不过 <code>acorn</code> 引擎只提供基本的解析 <code>ast</code> 的能力，遍历还需要配套的 <code>acorn-travesal</code>, 替换节点需要使用 acorn-，而这些开发，在 Babel 的插件体系开发下，变得一体化了（摘自 AlloyTeam 团队的剖析 babel）</p>
<h3 id="5-9-2-使用-babylon"><a href="#5-9-2-使用-babylon" class="headerlink" title="5.9.2 使用 babylon"></a>5.9.2 使用 babylon</h3><p>使用 babylon 编写一个数组 rest 转 Es5 语法的插件</p>
<p>把 <code>const arr = [ ...arr1, ...arr2 ]</code> 转成 <code>var arr = [].concat(arr1, arr2)</code></p>
<p>我们使用 babylon 的话就不需要使用 <code>@babel/core</code> 了，只需要用到他里面的 <code>traverse</code> 和 <code>generator</code>，用到的包有 <code>babylon、@babel/traverse、@babel/generator、@babel/types</code></p>
<h3 id="5-9-3-分析语法树"><a href="#5-9-3-分析语法树" class="headerlink" title="5.9.3 分析语法树"></a>5.9.3 分析语法树</h3><p>先来看一下两棵语法树的区别</p>
<p><img data-src="https://oscimg.oschina.net/oscnet/03608f83-7e7d-4de8-bfe5-3265ade3edd8.jpg" alt="img"></p>
<p>根据上图我们分析得出：</p>
<ol>
<li>两棵树都是变量声明的方式，不同的是他们声明的关键字不一样</li>
<li>他们初始化变量值的时候是不一样的，一个数组表达式（ArrayExpression）另一个是调用表达式（CallExpression）</li>
<li>那我们要做的就很简单了，就是把 数组表达式转换为调用表达式就可以</li>
</ol>
<h3 id="5-9-4-分析类型"><a href="#5-9-4-分析类型" class="headerlink" title="5.9.4 分析类型"></a>5.9.4 分析类型</h3><p>这段代码的核心生成一个 callExpression 调用表达式，所以对应官网上的类型，我们分析需要用到的 api</p>
<ul>
<li><p>先来分析 init 里面的，首先是 callExpression</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">&#x2F;**</span><br><span class="line"> * @param &#123;Expression&#125; callee  (required)</span><br><span class="line"> * @param &#123;Array&lt;Expression | SpreadElement | JSXNamespacedName&gt;&#125; source (required)</span><br><span class="line"> *&#x2F;</span><br><span class="line">t.callExpression(callee, arguments)</span><br></pre></td></tr></table></figure>
</li>
<li><p>对应语法树上 callee 是一个 MemberExpression，所以要生成一个成员表达式</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">&#x2F;**</span><br><span class="line"> * @param &#123;Expression&#125; object  (required)</span><br><span class="line"> * @param &#123;if computed then Expression else Identifier&#125; property (required)</span><br><span class="line"> * @param &#123;boolean&#125; computed (default: false)</span><br><span class="line"> * @param &#123;boolean&#125; optional (default: null)</span><br><span class="line"> *&#x2F;</span><br><span class="line">t.memberExpression(object, property, computed, optional)</span><br></pre></td></tr></table></figure>
</li>
<li><p>在 callee 的 object 是一个 ArrayExpression 数组表达式，是一个空数组</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">&#x2F;**</span><br><span class="line"> * @param &#123;Array&lt;null | Expression | SpreadElement&gt;&#125; elements  (default: [])</span><br><span class="line"> *&#x2F;</span><br><span class="line">t.arrayExpression(elements)</span><br></pre></td></tr></table></figure>
</li>
<li><p>对了里面的东西分析完了，我们还要生成 VariableDeclarator 和 VariableDeclaration 最终生成新的语法树</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">&#x2F;**</span><br><span class="line"> * @param &#123;LVal&#125; id  (required)</span><br><span class="line"> * @param &#123;Expression&#125; init (default: null)</span><br><span class="line"> *&#x2F;</span><br><span class="line">t.variableDeclarator(id, init)</span><br><span class="line"></span><br><span class="line">&#x2F;**</span><br><span class="line"> * @param &#123;&quot;var&quot; | &quot;let&quot; | &quot;const&quot;&#125; kind  (required)</span><br><span class="line"> * @param &#123;Array&lt;VariableDeclarator&gt;&#125; declarations (required)</span><br><span class="line"> *&#x2F;</span><br><span class="line">t.variableDeclaration(kind, declarations)</span><br></pre></td></tr></table></figure>
</li>
<li><p>其实倒着分析语法树，分析完怎么写也就清晰了，那么我们开始上代码吧</p>
</li>
</ul>
<h3 id="5-9-5-上代码"><a href="#5-9-5-上代码" class="headerlink" title="5.9.5 上代码"></a>5.9.5 上代码</h3><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br></pre></td><td class="code"><pre><span class="line">const babylon &#x3D; require(&#39;babylon&#39;)</span><br><span class="line">&#x2F;&#x2F; 使用 babel 提供的包，traverse 和 generator 都是被暴露在 default 对象上的</span><br><span class="line">const traverse &#x3D; require(&#39;@babel&#x2F;traverse&#39;).default</span><br><span class="line">const generator &#x3D; require(&#39;@babel&#x2F;generator&#39;).default</span><br><span class="line">const t &#x3D; require(&#39;@babel&#x2F;types&#39;)</span><br><span class="line"></span><br><span class="line">const code &#x3D; &#96;const arr &#x3D; [ ...arr1, ...arr2 ]&#96; &#x2F;&#x2F; var arr &#x3D; [].concat(arr1, arr2)</span><br><span class="line"></span><br><span class="line">const ast &#x3D; babylon.parse(code, &#123;</span><br><span class="line">  sourceType: &#39;module&#39;,</span><br><span class="line">&#125;)</span><br><span class="line"></span><br><span class="line">&#x2F;&#x2F; 转换树</span><br><span class="line">traverse(ast, &#123;</span><br><span class="line">  VariableDeclaration(path) &#123;</span><br><span class="line">    const node &#x3D; path.node</span><br><span class="line">    const declarations &#x3D; node.declarations</span><br><span class="line">    console.log(&#39;VariableDeclarator -&gt; declarations&#39;, declarations)</span><br><span class="line">    const kind &#x3D; &#39;var&#39;</span><br><span class="line">    &#x2F;&#x2F; 边界判定</span><br><span class="line">    if (node.kind !&#x3D;&#x3D; kind &amp;&amp; declarations.length &#x3D;&#x3D;&#x3D; 1 &amp;&amp; t.isArrayExpression(declarations[0].init)) &#123;</span><br><span class="line">      &#x2F;&#x2F; 取得之前的 elements</span><br><span class="line">      const args &#x3D; declarations[0].init.elements.map((item) &#x3D;&gt; item.argument)</span><br><span class="line">      const callee &#x3D; t.memberExpression(t.arrayExpression(), t.identifier(&#39;concat&#39;), false)</span><br><span class="line">      const init &#x3D; t.callExpression(callee, args)</span><br><span class="line">      const declaration &#x3D; t.variableDeclarator(declarations[0].id, init)</span><br><span class="line">      const variableDeclaration &#x3D; t.variableDeclaration(kind, [declaration])</span><br><span class="line">      path.replaceWith(variableDeclaration)</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;,</span><br><span class="line">&#125;)</span><br></pre></td></tr></table></figure>

<h2 id="5-10-具体语法树"><a href="#5-10-具体语法树" class="headerlink" title="5.10 具体语法树"></a>5.10 具体语法树</h2><p>和抽象语法树相对的是具体语法树（<code>Concrete Syntax Tree</code>）简称 <code>CST</code>（通常称作分析树）。一般的，在源代码的翻译和编译过程中，语法分析器创建出分析树。一旦 AST 被创建出来，在后续的处理过程中，比如语义分析阶段，会添加一些信息。可参考抽象语法树和具体语法树有什么区别？</p>
<h2 id="5-11-补充"><a href="#5-11-补充" class="headerlink" title="5.11 补充"></a>5.11 补充</h2><p>关于 node 类型，全集大致如下：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">(parameter) node: Identifier | SimpleLiteral | RegExpLiteral | Program | FunctionDeclaration | FunctionExpression | ArrowFunctionExpression | SwitchCase | CatchClause | VariableDeclarator | ExpressionStatement | BlockStatement | EmptyStatement | DebuggerStatement | WithStatement | ReturnStatement | LabeledStatement | BreakStatement | ContinueStatement | IfStatement | SwitchStatement | ThrowStatement | TryStatement | WhileStatement | DoWhileStatement | ForStatement | ForInStatement | ForOfStatement | VariableDeclaration | ClassDeclaration | ThisExpression | ArrayExpression | ObjectExpression | YieldExpression | UnaryExpression | UpdateExpression | BinaryExpression | AssignmentExpression | LogicalExpression | MemberExpression | ConditionalExpression | SimpleCallExpression | NewExpression | SequenceExpression | TemplateLiteral | TaggedTemplateExpression | ClassExpression | MetaProperty | AwaitExpression | Property | AssignmentProperty | Super | TemplateElement | SpreadElement | ObjectPattern | ArrayPattern | RestElement | AssignmentPattern | ClassBody | MethodDefinition | ImportDeclaration | ExportNamedDeclaration | ExportDefaultDeclaration | ExportAllDeclaration | ImportSpecifier | ImportDefaultSpecifier | ImportNamespaceSpecifier | ExportSpecifier</span><br></pre></td></tr></table></figure>

<p>Babel 有文档对 AST 树的详细定义，可参考这里</p>
<h2 id="5-12-配套源码地址"><a href="#5-12-配套源码地址" class="headerlink" title="5.12 配套源码地址"></a>5.12 配套源码地址</h2><p>代码以存放到 GitHub，地址：<a target="_blank" rel="noopener" href="https://github.com/fecym/ast-share">https://github.com/fecym/ast-share</a></p>
<h2 id="参考链接"><a href="#参考链接" class="headerlink" title="参考链接"></a>参考链接</h2><ol>
<li>JavaScript 语法解析、AST、V8、JIT</li>
<li>详解 AST 抽象语法树</li>
<li>AST 抽象语法树 ps: 这个里面有 class 转 Es5 构造函数的过程，有兴趣可以看一下</li>
<li>剖析 Babel——Babel 总览 | AlloyTeam</li>
<li>@babel/types</li>
</ol>
<h1 id="6-webpack-流程"><a href="#6-webpack-流程" class="headerlink" title="6.webpack-流程"></a>6.webpack-流程</h1><h2 id="6-1-引言"><a href="#6-1-引言" class="headerlink" title="6.1 引言"></a>6.1 引言</h2><p>目前，几乎所有业务的开发构建都会用到 webpack 。的确，作为模块加载和打包神器，只需配置几个文件，加载各种 loader 就可以享受无痛流程化开发。但对于 webpack 这样一个复杂度较高的插件集合，它的整体流程及思想对我们来说还是很透明的。那么接下来我会带你了解 webpack 这样一个构建黑盒，首先来谈谈它的流程。</p>
<h2 id="6-2-准备工作"><a href="#6-2-准备工作" class="headerlink" title="6.2 准备工作"></a>6.2 准备工作</h2><h3 id="6-2-1-webstorm-中配置-webpack-webstorm-debugger-script"><a href="#6-2-1-webstorm-中配置-webpack-webstorm-debugger-script" class="headerlink" title="6.2.1 webstorm 中配置 webpack-webstorm-debugger-script"></a>6.2.1 webstorm 中配置 webpack-webstorm-debugger-script</h3><p>在开始了解之前，必须要能对 webpack 整个流程进行 debug ，配置过程比较简单。</p>
<p>先将 <a target="_blank" rel="noopener" href="https://www.npmjs.com/package/webpack-webstorm-debugger-script">webpack-webstorm-debugger-script</a> 中的 <code>webstorm-debugger.js</code> 置于<code>webpack.config.js</code> 的同一目录下，搭建好你的脚手架后就可以直接 Debug 这个 webstorm-debugger.js 文件了。</p>
<h3 id="6-2-2-webpack-config-js-配置"><a href="#6-2-2-webpack-config-js-配置" class="headerlink" title="6.2.2 webpack.config.js 配置"></a>6.2.2 webpack.config.js 配置</h3><p>估计大家对 webpack.config.js 的配置也尝试过不少次了，这里就大致对这个配置文件进行个分析。</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br></pre></td><td class="code"><pre><span class="line">var path &#x3D; require(&#39;path&#39;);</span><br><span class="line">var node_modules &#x3D; path.resolve(__dirname, &#39;node_modules&#39;);</span><br><span class="line">var pathToReact &#x3D; path.resolve(node_modules, &#39;react&#x2F;dist&#x2F;react.min.js&#39;);</span><br><span class="line">module.exports &#x3D; &#123;</span><br><span class="line">	&#x2F;&#x2F; 入口文件，是模块构建的起点，同时每一个入口文件对应最后生成的一个 chunk。</span><br><span class="line">	entry: &#123;</span><br><span class="line">		bundle: [</span><br><span class="line">        	&#39;webpack&#x2F;hot&#x2F;dev-server&#39;,</span><br><span class="line">        	&#39;webpack-dev-server&#x2F;client?http:&#x2F;&#x2F;localhost:8080&#39;,</span><br><span class="line">        	path.resolve(__dirname, &#39;app&#x2F;app.js&#39;)</span><br><span class="line">  		]</span><br><span class="line">  	&#125;,</span><br><span class="line">  	&#x2F;&#x2F; 文件路径指向(可加快打包过程)。</span><br><span class="line">  	resolve: &#123; </span><br><span class="line">  		alias: &#123;</span><br><span class="line">  			&#39;react&#39;: pathToReact</span><br><span class="line">        &#125;</span><br><span class="line"> 	&#125;,</span><br><span class="line"> 	&#x2F;&#x2F; 生成文件，是模块构建的终点，包括输出文件与输出路径。</span><br><span class="line"> 	output: &#123;</span><br><span class="line"> 		path: path.resolve(__dirname, &#39;build&#39;),</span><br><span class="line"> 		filename: &#39;[name].js&#39;</span><br><span class="line">    &#125;,</span><br><span class="line">    &#x2F;&#x2F; 这里配置了处理各模块的 loader ，包括 css 预处理 loader ，es6 编译 loader，图片处理 loader。</span><br><span class="line">    module: &#123;</span><br><span class="line">    	loaders: [</span><br><span class="line">    		&#123; </span><br><span class="line">    			test: &#x2F;\.js$&#x2F;,</span><br><span class="line">    			loader: &#39;babel&#39;,</span><br><span class="line">    			query: &#123;</span><br><span class="line">    				presets: [&#39;es2015&#39;, &#39;react&#39;]</span><br><span class="line">             	&#125;</span><br><span class="line">            &#125;</span><br><span class="line">    	],</span><br><span class="line">    	noParse: [pathToReact]</span><br><span class="line">    &#125;,</span><br><span class="line">    &#x2F;&#x2F; webpack 各插件对象，在 webpack 的事件流中执行对应的方法。</span><br><span class="line">    plugins: [</span><br><span class="line">    	new webpack.HotModuleReplacementPlugin()</span><br><span class="line">    ]</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure>

<p>除此之外再大致介绍下 webpack 的一些核心概念：</p>
<ul>
<li>loader：能转换各类资源，并处理成对应模块的加载器。loader 间可以串行使用。</li>
<li>chunk：code splitting 后的产物，也就是按需加载的分块，装载了不同的 module。</li>
</ul>
<p>对于 module 和 chunk 的关系可以参照 webpack 官方的这张图：</p>
<p><img data-src="https://img.alicdn.com/tps/TB1B0DXNXXXXXXdXFXXXXXXXXXX-368-522.jpg" alt="img"></p>
<ul>
<li>plugin：webpack 的插件实体，这里以 UglifyJsPlugin 为例。</li>
</ul>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line">function UglifyJsPlugin(options) &#123;</span><br><span class="line">	this.options &#x3D; options;</span><br><span class="line">&#125;</span><br><span class="line">module.exports &#x3D; UglifyJsPlugin;</span><br><span class="line">UglifyJsPlugin.prototype.apply &#x3D; function(compiler) &#123;</span><br><span class="line">	compiler.plugin(&quot;compilation&quot;, function(compilation) &#123;</span><br><span class="line">		compilation.plugin(&quot;build-module&quot;,</span><br><span class="line">        function(module) &#123;</span><br><span class="line">        </span><br><span class="line">        &#125;);</span><br><span class="line">		compilation.plugin(&quot;optimize-chunk-assets&quot;, </span><br><span class="line">		function(chunks, callback) &#123;</span><br><span class="line">			&#x2F;&#x2F; Uglify 逻辑</span><br><span class="line">       	&#125;);</span><br><span class="line">       	compilation.plugin(&quot;normal-module-loader&quot;, function(context) &#123;</span><br><span class="line">       	</span><br><span class="line">       	&#125;);</span><br><span class="line">   	&#125;);</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure>

<p>  在 webpack 中你经常可以看到 compilation.plugin(‘xxx’, callback) ，你可以把它当作是一个事件的绑定，这些事件在打包时由 webpack 来触发。</p>
<h3 id="6-2-3-流程总览"><a href="#6-2-3-流程总览" class="headerlink" title="6.2.3 流程总览"></a>6.2.3 流程总览</h3><p>在具体流程学习前，可以先通过这幅 <a target="_blank" rel="noopener" href="https://img.alicdn.com/tps/TB1GVGFNXXXXXaTapXXXXXXXXXX-4436-4244.jpg">webpack 整体流程图</a> 了解一下大致流程（建议保存下来查看）。</p>
<p><img data-src="https://img.alicdn.com/tps/TB1GVGFNXXXXXaTapXXXXXXXXXX-4436-4244.jpg" alt="img"></p>
<h2 id="6-3-shell-与-config-解析"><a href="#6-3-shell-与-config-解析" class="headerlink" title="6.3 shell 与 config 解析"></a>6.3 shell 与 config 解析</h2><p>每次在命令行输入 webpack 后，操作系统都会去调用 <code>./node_modules/.bin/webpack</code> 这个 shell 脚本。这个脚本会去调用 <code>./node_modules/webpack/bin/webpack.js</code> 并追加输入的参数，如 -p , -w 。(图中 webpack.js 是 webpack 的启动文件，而 $@ 是后缀参数)</p>
<p><img data-src="https://img.alicdn.com/tps/TB1kvfbNXXXXXarXpXXXXXXXXXX-500-111.jpg" alt="img"></p>
<p>在 webpack.js 这个文件中 webpack 通过 optimist 将用户配置的 webpack.config.js 和 shell 脚本传过来的参数整合成 options 对象传到了下一个流程的控制对象中。</p>
<h3 id="6-3-1-optimist"><a href="#6-3-1-optimist" class="headerlink" title="6.3.1 optimist"></a>6.3.1 optimist</h3><p>和 commander 一样，<a target="_blank" rel="noopener" href="https://github.com/substack/node-optimist">optimist</a> 实现了 node 命令行的解析，其 API 调用非常方便。</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">var optimist &#x3D; require(&quot;optimist&quot;);</span><br><span class="line">optimist  .boolean(&quot;json&quot;).alias(&quot;json&quot;, &quot;j&quot;).describe(&quot;json&quot;)  .boolean(&quot;colors&quot;).alias(&quot;colors&quot;, &quot;c&quot;).describe(&quot;colors&quot;)  .boolean(&quot;watch&quot;).alias(&quot;watch&quot;, &quot;w&quot;).describe(&quot;watch&quot;)  ...</span><br></pre></td></tr></table></figure>

<p>获取到后缀参数后，optimist 分析参数并以键值对的形式把参数对象保存在 optimist.argv 中，来看看 argv 究竟有什么？</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">&#x2F;&#x2F; webpack --hot -w&#123;  hot: true,  profile: false,  watch: true,  ...&#125;</span><br></pre></td></tr></table></figure>

<h3 id="6-3-2-config-合并与插件加载"><a href="#6-3-2-config-合并与插件加载" class="headerlink" title="6.3.2 config 合并与插件加载"></a>6.3.2 config 合并与插件加载</h3><p>在加载插件之前，webpack 将 webpack.config.js 中的各个配置项拷贝到 options 对象中，并加载用户配置在 webpack.config.js 的 plugins 。接着 optimist.argv 会被传入到<code>./node_modules/webpack/bin/convert-argv.js</code> 中，通过判断 argv 中参数的值决定是否去加载对应插件。(至于 webpack 插件运行机制，在之后的运行机制篇会提到)</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">ifBooleanArg(&quot;hot&quot;, function() &#123;</span><br><span class="line">	ensureArray(options, &quot;plugins&quot;);</span><br><span class="line">	var HotModuleReplacementPlugin &#x3D; require(&quot;..&#x2F;lib&#x2F;HotModuleReplacementPlugin&quot;);</span><br><span class="line">	options.plugins.push(new HotModuleReplacementPlugin());</span><br><span class="line">&#125;);</span><br><span class="line">...</span><br><span class="line">return options;</span><br></pre></td></tr></table></figure>

<p><code>options</code> 作为最后返回结果，包含了之后构建阶段所需的重要信息。</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">	entry: &#123;&#125;,</span><br><span class="line">	&#x2F;&#x2F;入口配置</span><br><span class="line">	output: &#123;&#125;,</span><br><span class="line">	&#x2F;&#x2F;输出配置</span><br><span class="line">	plugins: [],</span><br><span class="line">	&#x2F;&#x2F;插件集合(配置文件 + shell指令)</span><br><span class="line">	module: &#123; loaders: [ [Object] ] &#125;,</span><br><span class="line">	&#x2F;&#x2F;模块配置</span><br><span class="line">	context: </span><br><span class="line">	&#x2F;&#x2F;工程路径</span><br><span class="line">	...</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>这和 webpack.config.js 的配置非常相似，只是多了一些经 shell 传入的插件对象。插件对象一初始化完毕， options 也就传入到了下个流程中。</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">var webpack &#x3D; require(&quot;..&#x2F;lib&#x2F;webpack.js&quot;);var compiler &#x3D; webpack(options);</span><br></pre></td></tr></table></figure>

<h3 id="6-3-3-编译与构建流程"><a href="#6-3-3-编译与构建流程" class="headerlink" title="6.3.3 编译与构建流程"></a>6.3.3 编译与构建流程</h3><p>在加载配置文件和 shell 后缀参数申明的插件，并传入构建信息 options 对象后，开始整个 webpack 打包最漫长的一步。而这个时候，真正的 webpack 对象才刚被初始化，具体的初始化逻辑在 <code>lib/webpack.js</code> 中，如下：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">function webpack(options) &#123;</span><br><span class="line">	var compiler &#x3D; new Compiler();</span><br><span class="line">	...</span><br><span class="line">	&#x2F;&#x2F; 检查options,若watch字段为true,则开启watch线程</span><br><span class="line">	return compiler;</span><br><span class="line">&#125;</span><br><span class="line">...</span><br></pre></td></tr></table></figure>

<p>webpack 的实际入口是 Compiler 中的 run 方法，run 一旦执行后，就开始了编译和构建流程 ，其中有几个比较关键的 webpack 事件节点。</p>
<ul>
<li><code>compile</code> 开始编译</li>
<li><code>make</code> 从入口点分析模块及其依赖的模块，创建这些模块对象</li>
<li><code>build-module</code> 构建模块</li>
<li><code>after-compile</code> 完成构建</li>
<li><code>seal</code> 封装构建结果</li>
<li><code>emit</code> 把各个chunk输出到结果文件</li>
<li><code>after-emit</code> 完成输出</li>
</ul>
<h4 id="1-核心对象-Compilation"><a href="#1-核心对象-Compilation" class="headerlink" title="1. 核心对象 Compilation"></a>1. 核心对象 Compilation</h4><p>compiler.run 后首先会触发 compile ，这一步会构建出 Compilation 对象：</p>
<p><img data-src="https://img.alicdn.com/tps/TB1UgS4NXXXXXXZXVXXXXXXXXXX-693-940.png" alt="compilation类图"></p>
<p>这个对象有两个作用，一是负责组织整个打包过程，包含了每个构建环节及输出环节所对应的方法，可以从图中看到比较关键的步骤，如 <code>addEntry()</code> , <code>_addModuleChain()</code> ,<code>buildModule()</code> , <code>seal()</code> , <code>createChunkAssets()</code> (在每一个节点都会触发 webpack 事件去调用各插件)。二是该对象内部存放着所有 module ，chunk，生成的 asset 以及用来生成最后打包文件的 template 的信息。</p>
<h4 id="2-编译与构建主流程"><a href="#2-编译与构建主流程" class="headerlink" title="2. 编译与构建主流程"></a>2. 编译与构建主流程</h4><p>在创建 module 之前，Compiler 会触发 make，并调用 <code>Compilation.addEntry</code> 方法，通过 options 对象的 entry 字段找到我们的入口js文件。之后，在 addEntry 中调用私有方法<code>_addModuleChain</code> ，这个方法主要做了两件事情。一是根据模块的类型获取对应的模块工厂并创建模块，二是构建模块。</p>
<p>而构建模块作为最耗时的一步，又可细化为三步：</p>
<ul>
<li><p>调用各 loader 处理模块之间的依赖</p>
<p>webpack 提供的一个很大的便利就是能将所有资源都整合成模块，不仅仅是 js 文件。所以需要一些 loader ，比如 <code>url-loader</code> ， <code>jsx-loader</code> ， <code>css-loader</code> 等等来让我们可以直接在源文件中引用各类资源。webpack 调用 <code>doBuild()</code> ，对每一个 require() 用对应的 loader 进行加工，最后生成一个 js module。</p>
</li>
<li><p>调用 <a target="_blank" rel="noopener" href="https://github.com/ternjs/acorn">acorn</a> 解析经 loader 处理后的源文件生成抽象语法树 AST</p>
</li>
</ul>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line">Parser.prototype.parse &#x3D; function parse(source, initialState) &#123;</span><br><span class="line">    var ast;</span><br><span class="line">    if (!ast) &#123;</span><br><span class="line">        &#x2F;&#x2F; acorn以es6的语法进行解析</span><br><span class="line">        ast &#x3D; acorn.parse(source, &#123;</span><br><span class="line">            ranges: true,</span><br><span class="line">            locations: true,</span><br><span class="line">            ecmaVersion: 6,</span><br><span class="line">            sourceType: &quot;module&quot;</span><br><span class="line">        &#125;);</span><br><span class="line">    &#125;</span><br><span class="line">    ...</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure>

<ul>
<li><p>遍历 AST，构建该模块所依赖的模块</p>
<p>对于当前模块，或许存在着多个依赖模块。当前模块会开辟一个依赖模块的数组，在遍历 AST 时，将 require() 中的模块通过 <code>addDependency()</code> 添加到数组中。当前模块构建完成后，webpack 调用 <code>processModuleDependencies</code> 开始递归处理依赖的 module，接着就会重复之前的构建步骤。</p>
<p><code> </code></p>
</li>
</ul>
<h4 id="3-构建细节"><a href="#3-构建细节" class="headerlink" title="3. 构建细节"></a>3. 构建细节</h4><p>module 是 webpack 构建的核心实体，也是所有 module 的 父类，它有几种不同子类：<code>NormalModule</code> , <code>MultiModule</code> , <code>ContextModule</code> , <code>DelegatedModule</code> 等。但这些核心实体都是在构建中都会去调用对应方法，也就是 <code>build()</code> 。来看看其中具体做了什么：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br></pre></td><td class="code"><pre><span class="line">&#x2F;&#x2F; 初始化module信息，如context,id,chunks,dependencies等。</span><br><span class="line">NormalModule.prototype.build &#x3D; function build(options, compilation, resolver, fs, callback) &#123;</span><br><span class="line">	this.buildTimestamp &#x3D; new Date().getTime();</span><br><span class="line">	&#x2F;&#x2F; 构建计时</span><br><span class="line">	this.built &#x3D; true;</span><br><span class="line">	return this.doBuild(options, compilation, resolver, fs, function(err) &#123;</span><br><span class="line">		&#x2F;&#x2F; 指定模块引用，不经acorn解析</span><br><span class="line">		if (options.module &amp;&amp; options.module.noParse) &#123;</span><br><span class="line">			if (Array.isArray(options.module.noParse)) &#123;</span><br><span class="line">                if (options.module.noParse.some(function(regExp) &#123;</span><br><span class="line">                	return typeof regExp &#x3D;&#x3D;&#x3D; &quot;string&quot; ? this.request.indexOf(regExp) &#x3D;&#x3D;&#x3D; 0 : regExp.test(this.request);</span><br><span class="line">                &#125;, this)) &#123;</span><br><span class="line">                	return callback();</span><br><span class="line">             	&#125;</span><br><span class="line">        	&#125; else if (typeof options.module.noParse &#x3D;&#x3D;&#x3D; &quot;string&quot; ? this.request.indexOf(options.module.noParse) &#x3D;&#x3D;&#x3D; 0 : options.module.noParse.test(this.request)) &#123;</span><br><span class="line">        		return callback();</span><br><span class="line">        	&#125;</span><br><span class="line">        &#125;</span><br><span class="line">        &#x2F;&#x2F; 由acorn解析生成ast</span><br><span class="line">        try &#123;</span><br><span class="line">        	this.parser.parse(this._source.source(), &#123;</span><br><span class="line">        		current: this,</span><br><span class="line">        		module: this,</span><br><span class="line">        		compilation: compilation,</span><br><span class="line">        		options: options</span><br><span class="line">        	&#125;);</span><br><span class="line">       	&#125; catch (e) &#123;</span><br><span class="line">       		var source &#x3D; this._source.source();</span><br><span class="line">       		this._source &#x3D; null;</span><br><span class="line">       		return callback(new ModuleParseError(this, source, e));</span><br><span class="line">      	&#125;</span><br><span class="line">      	return callback();</span><br><span class="line"> 	&#125;.bind(this));</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure>

<p>对于每一个 module ，它都会有这样一个构建方法。当然，它还包括了从构建到输出的一系列的有关 module 生命周期的函数，我们通过 module 父类类图其子类类图(这里以 NormalModule 为例)来观察其真实形态：</p>
<p><img data-src="https://img.alicdn.com/tps/TB1WOiRNXXXXXcJaXXXXXXXXXXX-445-1228.png" alt="module类图"></p>
<p>可以看到无论是构建流程，处理依赖流程，包括后面的封装流程都是与 module 密切相关的。</p>
<h2 id="6-4-打包输出"><a href="#6-4-打包输出" class="headerlink" title="6.4 打包输出"></a>6.4 打包输出</h2><p>在所有模块及其依赖模块 build 完成后，webpack 会监听 <code>seal</code> 事件调用各插件对构建后的结果进行封装，要逐次对每个 module 和 chunk 进行整理，生成编译后的源码，合并，拆分，生成 hash 。 同时这是我们在开发时进行代码优化和功能添加的关键环节。</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br></pre></td><td class="code"><pre><span class="line">Compilation.prototype.seal &#x3D; function seal(callback) &#123;</span><br><span class="line">	this.applyPlugins(&quot;seal&quot;);</span><br><span class="line">	&#x2F;&#x2F; 触发插件的seal事件</span><br><span class="line">	this.preparedChunks.sort(function(a, b) &#123;</span><br><span class="line">		if (a.name &lt; b.name) &#123;</span><br><span class="line">			return -1;</span><br><span class="line">       	&#125;</span><br><span class="line">       	if (a.name &gt; b.name) &#123;</span><br><span class="line">       		return 1;</span><br><span class="line">		&#125;</span><br><span class="line">		return 0;</span><br><span class="line">	&#125;);</span><br><span class="line">	this.preparedChunks.forEach(function(preparedChunk) &#123;</span><br><span class="line">		var module &#x3D; preparedChunk.module;</span><br><span class="line">		var chunk &#x3D; this.addChunk(preparedChunk.name, module);</span><br><span class="line">		chunk.initial &#x3D; chunk.entry &#x3D; true;</span><br><span class="line">		&#x2F;&#x2F; 整理每个Module和chunk，每个chunk对应一个输出文件。</span><br><span class="line">		chunk.addModule(module);</span><br><span class="line">		module.addChunk(chunk);</span><br><span class="line">	&#125;, this); </span><br><span class="line">	this.applyPluginsAsync(&quot;optimize-tree&quot;, this.chunks, this.modules, function(err) &#123;</span><br><span class="line">		if (err) &#123;</span><br><span class="line">			return callback(err);</span><br><span class="line">		&#125;</span><br><span class="line">		...</span><br><span class="line">		&#x2F;&#x2F; 触发插件的事件</span><br><span class="line">		this.createChunkAssets();</span><br><span class="line">		&#x2F;&#x2F; 生成最终assets</span><br><span class="line">		...</span><br><span class="line">		&#x2F;&#x2F; 触发插件的事件</span><br><span class="line">	&#125;.bind(this));</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure>

<h4 id="1-生成最终-assets"><a href="#1-生成最终-assets" class="headerlink" title="1. 生成最终 assets"></a>1. 生成最终 assets</h4><p>在封装过程中，webpack 会调用 Compilation 中的 <code>createChunkAssets</code> 方法进行打包后代码的生成。 createChunkAssets 流程如下：</p>
<p><img data-src="https://img.alicdn.com/tps/TB1cz5.NXXXXXc7XpXXXXXXXXXX-959-807.png" alt="createChunkAssets流程"></p>
<ul>
<li><p>不同的 Template</p>
<p>从上图可以看出通过判断是入口 js 还是需要异步加载的 js 来选择不同的模板对象进行封装，入口 js 会采用 webpack 事件流的 render 事件来触发 <code>Template类</code> 中的<code>renderChunkModules()</code> (异步加载的 js 会调用 chunkTemplate 中的 render 方法)。</p>
</li>
</ul>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">if(chunk.entry) &#123;</span><br><span class="line">	source &#x3D; this.mainTemplate.render(this.hash, chunk, this.moduleTemplate, this.dependencyTemplates);</span><br><span class="line">&#125; else &#123;</span><br><span class="line">	source &#x3D; this.chunkTemplate.render(chunk, this.moduleTemplate, this.dependencyTemplates);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>  在 webpack 中有四个 Template 的子类，分别是 <code>MainTemplate.js</code> ， <code>ChunkTemplate.js</code>，<code>ModuleTemplate.js</code> ， <code>HotUpdateChunkTemplate.js</code> ，前两者先前已大致有介绍，而 ModuleTemplate 是对所有模块进行一个代码生成，HotUpdateChunkTemplate 是对热替换模块的一个处理。</p>
<ul>
<li><p>模块封装</p>
<p>模块在封装的时候和它在构建时一样，都是调用各模块类中的方法。封装通过调用<code>module.source()</code> 来进行各操作，比如说 require() 的替换。</p>
</li>
</ul>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">MainTemplate.prototype.requireFn &#x3D; &quot;__webpack_require__&quot;;</span><br><span class="line">MainTemplate.prototype.render &#x3D; function(hash, chunk, moduleTemplate, dependencyTemplates) &#123;</span><br><span class="line">	var buf &#x3D; [];</span><br><span class="line">	&#x2F;&#x2F; 每一个module都有一个moduleId,在最后会替换。</span><br><span class="line">	buf.push(&quot;function &quot; + this.requireFn + &quot;(moduleId) &#123;&quot;);</span><br><span class="line">	buf.push(this.indent(this.applyPluginsWaterfall(&quot;require&quot;, &quot;&quot;, chunk, hash)));</span><br><span class="line">	buf.push(&quot;&#125;&quot;);</span><br><span class="line">	buf.push(&quot;&quot;);</span><br><span class="line">	... </span><br><span class="line">	&#x2F;&#x2F; 其余封装操作</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure>

<ul>
<li><p>生成 assets</p>
<p>各模块进行 doBlock 后，把 module 的最终代码循环添加到 source 中。一个 source 对应着一个 asset 对象，该对象保存了单个文件的文件名( name )和最终代码( value )。</p>
</li>
</ul>
<h4 id="2-输出"><a href="#2-输出" class="headerlink" title="2. 输出"></a>2. 输出</h4><p>最后一步，webpack 调用 Compiler 中的 <code>emitAssets()</code> ，按照 output 中的配置项将文件输出到了对应的 path 中，从而 webpack 整个打包过程结束。要注意的是，若想对结果进行处理，则需要在 <code>emit</code> 触发后对自定义插件进行扩展。</p>
<h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>webpack 的整体流程主要还是依赖于 <code>compilation</code> 和 <code>module</code> 这两个对象，但其思想远不止这么简单。最开始也说过，webpack 本质是个插件集合，并且由 <code>tapable</code> 控制各插件在 webpack 事件流上运行，至于具体的思想和细节，将会在后一篇文章中提到。同时，在业务开发中，无论是为了提升构建效率，或是减小打包文件大小，我们都可以通过编写 webpack 插件来进行流程上的控制，这个也会在之后提到。</p>
<h1 id="7-webpack-插件"><a href="#7-webpack-插件" class="headerlink" title="7.webpack-插件"></a>7.webpack-插件</h1><h2 id="7-1-前言"><a href="#7-1-前言" class="headerlink" title="7.1 前言"></a>7.1 前言</h2><p>webpack本身并不难，他所完成的各种复杂炫酷的功能都依赖于他的插件机制。或许我们在日常的开发需求中并不需要自己动手写一个插件，然而，了解其中的机制也是一种学习的方向，当插件出现问题时，我们也能够自己来定位。</p>
<h2 id="7-2-Tapable"><a href="#7-2-Tapable" class="headerlink" title="7.2 Tapable"></a>7.2 Tapable</h2><p>Webpack的插件机制依赖于一个核心的库， <strong>Tapable</strong>。<br> 在深入webpack的插件机制之前，需要对该核心库有一定的了解。</p>
<h3 id="7-2-1-Tapable是什么"><a href="#7-2-1-Tapable是什么" class="headerlink" title="7.2.1 Tapable是什么"></a>7.2.1 Tapable是什么</h3><p>tapable 是一个类似于nodejs 的EventEmitter 的库, 主要是控制钩子函数的发布与订阅。当然，tapable提供的hook机制比较全面，分为同步和异步两个大类(异步中又区分异步并行和异步串行)，而根据事件执行的终止条件的不同，由衍生出 Bail/Waterfall/Loop 类型。</p>
<h3 id="7-2-2-Tapable的使用-（该小段内容引用文章）"><a href="#7-2-2-Tapable的使用-（该小段内容引用文章）" class="headerlink" title="7.2.2 Tapable的使用 （该小段内容引用文章）"></a>7.2.2 Tapable的使用 （该小段内容引用<a target="_blank" rel="noopener" href="https://juejin.im/post/6844903750729990152">文章</a>）</h3><p><strong>基本使用</strong>：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line">const &#123;</span><br><span class="line">  SyncHook</span><br><span class="line">&#125; &#x3D; require(&#39;tapable&#39;)</span><br><span class="line"></span><br><span class="line">&#x2F;&#x2F; 创建一个同步 Hook，指定参数</span><br><span class="line">const hook &#x3D; new SyncHook([&#39;arg1&#39;, &#39;arg2&#39;])</span><br><span class="line"></span><br><span class="line">&#x2F;&#x2F; 注册</span><br><span class="line">hook.tap(&#39;a&#39;, function (arg1, arg2) &#123;</span><br><span class="line">	console.log(&#39;a&#39;)</span><br><span class="line">&#125;)</span><br><span class="line"></span><br><span class="line">hook.tap(&#39;b&#39;, function (arg1, arg2) &#123;</span><br><span class="line">	console.log(&#39;b&#39;)</span><br><span class="line">&#125;)</span><br><span class="line"></span><br><span class="line">hook.call(1, 2)</span><br></pre></td></tr></table></figure>

<p><strong>钩子类型</strong>：</p>
<p><img data-src="https://user-gold-cdn.xitu.io/2018/12/28/167f458ac2b1e527?imageView2/0/w/1280/h/960/format/webp/ignore-error/1" alt="image"></p>
<p><img data-src="https://user-gold-cdn.xitu.io/2018/12/28/167f458d6ff8424f?imageView2/0/w/1280/h/960/format/webp/ignore-error/1" alt="image"></p>
<p><strong>BasicHook</strong>：执行每一个，不关心函数的返回值，有SyncHook、AsyncParallelHook、AsyncSeriesHook。</p>
<p><strong>BailHook</strong>：顺序执行 Hook，遇到第一个结果result!==undefined则返回，不再继续执行。有：SyncBailHook、AsyncSeriseBailHook, AsyncParallelBailHook。</p>
<p>什么样的场景下会使用到 BailHook 呢？设想如下一个例子：假设我们有一个模块 M，如果它满足 A 或者 B 或者 C 三者任何一个条件，就将其打包为一个单独的。这里的 A、B、C 不存在先后顺序，那么就可以使用 AsyncParallelBailHook 来解决:</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line">x.hooks.拆分模块的Hook.tap(&#39;A&#39;, () &#x3D;&gt; &#123;</span><br><span class="line">   if (A 判断条件满足) &#123;</span><br><span class="line">     return true</span><br><span class="line">   &#125;</span><br><span class="line"> &#125;)</span><br><span class="line"> x.hooks.拆分模块的Hook.tap(&#39;B&#39;, () &#x3D;&gt; &#123;</span><br><span class="line">   if (B 判断条件满足) &#123;</span><br><span class="line">     return true</span><br><span class="line">   &#125;</span><br><span class="line"> &#125;)</span><br><span class="line"> x.hooks.拆分模块的Hook.tap(&#39;C&#39;, () &#x3D;&gt; &#123;</span><br><span class="line">   if (C 判断条件满足) &#123;</span><br><span class="line">     return true</span><br><span class="line">   &#125;</span><br><span class="line"> &#125;)</span><br></pre></td></tr></table></figure>

<p>如果 A 中返回为 true，那么就无须再去判断 B 和 C。 但是当 A、B、C 的校验，需要严格遵循先后顺序时，就需要使用有顺序的 SyncBailHook(A、B、C 是同步函数时使用) 或者 AsyncSeriseBailHook(A、B、C 是异步函数时使用)。</p>
<p><strong>WaterfallHook</strong>：类似于 reduce，如果前一个 Hook 函数的结果 result !== undefined，则 result 会作为后一个 Hook 函数的第一个参数。既然是顺序执行，那么就只有 Sync 和 AsyncSeries 类中提供这个Hook：SyncWaterfallHook，AsyncSeriesWaterfallHook 当一个数据，需要经过 A，B，C 三个阶段的处理得到最终结果，并且 A 中如果满足条件 a 就处理，否则不处理，B 和 C 同样，那么可以使用如下</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><span class="line">x.hooks.tap(&#39;A&#39;, (data) &#x3D;&gt; &#123;</span><br><span class="line">   if (满足 A 需要处理的条件) &#123;</span><br><span class="line">     &#x2F;&#x2F; 处理数据 data</span><br><span class="line">     return data</span><br><span class="line">   &#125; else &#123;</span><br><span class="line">     return</span><br><span class="line">   &#125;</span><br><span class="line"> &#125;)</span><br><span class="line">x.hooks.tap(&#39;B&#39;, (data) &#x3D;&gt; &#123;</span><br><span class="line">   if (满足B需要处理的条件) &#123;</span><br><span class="line">     &#x2F;&#x2F; 处理数据 data</span><br><span class="line">     return data</span><br><span class="line">   &#125; else &#123;</span><br><span class="line">     return</span><br><span class="line">   &#125;</span><br><span class="line"> &#125;)</span><br><span class="line"> x.hooks.tap(&#39;C&#39;, (data) &#x3D;&gt; &#123;</span><br><span class="line">   if (满足 C 需要处理的条件) &#123;</span><br><span class="line">     &#x2F;&#x2F; 处理数据 data</span><br><span class="line">     return data</span><br><span class="line">   &#125; else &#123;</span><br><span class="line">     return</span><br><span class="line">   &#125;</span><br><span class="line"> &#125;)</span><br></pre></td></tr></table></figure>

<p><strong>LoopHook</strong>：不停的循环执行 Hook，直到所有函数结果 result === undefined。同样的，由于对串行性有依赖，所以只有 SyncLoopHook 和 AsyncSeriseLoopHook （PS：暂时没看到具体使用 Case）</p>
<h3 id="7-2-3-Tapable的源码分析"><a href="#7-2-3-Tapable的源码分析" class="headerlink" title="7.2.3 Tapable的源码分析"></a>7.2.3 Tapable的源码分析</h3><p>Tapable 基本逻辑是，先通过类实例的 tap 方法注册对应 Hook 的处理函数， 这里直接分析sync同步钩子的主要流程，其他的异步钩子和拦截器等就不赘述了。</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">const hook &#x3D; new SyncHook([&#39;arg1&#39;, &#39;arg2&#39;])</span><br></pre></td></tr></table></figure>

<p>从该句代码， 作为源码分析的入口，</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line">class SyncHook extends Hook &#123;</span><br><span class="line">    &#x2F;&#x2F; 错误处理，防止调用者调用异步钩子</span><br><span class="line">	tapAsync() &#123;</span><br><span class="line">		throw new Error(&quot;tapAsync is not supported on a SyncHook&quot;);</span><br><span class="line">	&#125;</span><br><span class="line">    &#x2F;&#x2F; 错误处理，防止调用者调用promise钩子</span><br><span class="line">	tapPromise() &#123;</span><br><span class="line">		throw new Error(&quot;tapPromise is not supported on a SyncHook&quot;);</span><br><span class="line">	&#125;</span><br><span class="line">    &#x2F;&#x2F; 核心实现</span><br><span class="line">	compile(options) &#123;</span><br><span class="line">		factory.setup(this, options);</span><br><span class="line">		return factory.create(options);</span><br><span class="line">	&#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>从类SyncHook看到， 他是继承于一个基类Hook， 他的核心实现compile等会再讲， 我们先看看基类Hook</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">&#x2F;&#x2F; 变量的初始化</span><br><span class="line">constructor(args) &#123;</span><br><span class="line">	if (!Array.isArray(args)) args &#x3D; [];</span><br><span class="line">	this._args &#x3D; args;</span><br><span class="line">	this.taps &#x3D; [];</span><br><span class="line">	this.interceptors &#x3D; [];</span><br><span class="line">	this.call &#x3D; this._call;</span><br><span class="line">	this.promise &#x3D; this._promise;</span><br><span class="line">	this.callAsync &#x3D; this._callAsync;</span><br><span class="line">	this._x &#x3D; undefined;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>初始化完成后， 通常会注册一个事件， 如：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">&#x2F;&#x2F; 注册</span><br><span class="line">hook.tap(&#39;a&#39;, function (arg1, arg2) &#123;</span><br><span class="line">	console.log(&#39;a&#39;)</span><br><span class="line">&#125;)</span><br><span class="line"></span><br><span class="line">hook.tap(&#39;b&#39;, function (arg1, arg2) &#123;</span><br><span class="line">	console.log(&#39;b&#39;)</span><br><span class="line">&#125;)</span><br></pre></td></tr></table></figure>

<p>很明显， 这两个语句都会调用基类中的tap方法：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line">tap(options, fn) &#123;</span><br><span class="line">    &#x2F;&#x2F; 参数处理</span><br><span class="line">	if (typeof options &#x3D;&#x3D;&#x3D; &quot;string&quot;) options &#x3D; &#123; name: options &#125;;</span><br><span class="line">	if (typeof options !&#x3D;&#x3D; &quot;object&quot; || options &#x3D;&#x3D;&#x3D; null)</span><br><span class="line">		throw new Error(</span><br><span class="line">			&quot;Invalid arguments to tap(options: Object, fn: function)&quot;</span><br><span class="line">		);</span><br><span class="line">	options &#x3D; Object.assign(&#123; type: &quot;sync&quot;, fn: fn &#125;, options);</span><br><span class="line">	if (typeof options.name !&#x3D;&#x3D; &quot;string&quot; || options.name &#x3D;&#x3D;&#x3D; &quot;&quot;)</span><br><span class="line">		throw new Error(&quot;Missing name for tap&quot;);</span><br><span class="line">	&#x2F;&#x2F; 执行拦截器的register函数， 比较简单不分析</span><br><span class="line">	options &#x3D; this._runRegisterInterceptors(options);</span><br><span class="line">	&#x2F;&#x2F; 处理注册事件</span><br><span class="line">	this._insert(options);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>从上面的源码分析， 可以看到_insert方法是注册阶段的关键函数， 直接进入该方法内部</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br></pre></td><td class="code"><pre><span class="line">_insert(item) &#123;</span><br><span class="line">    &#x2F;&#x2F; 重置所有的 调用 方法</span><br><span class="line">	this._resetCompilation();</span><br><span class="line">	&#x2F;&#x2F; 将注册事件排序后放进taps数组</span><br><span class="line">	let before;</span><br><span class="line">	if (typeof item.before &#x3D;&#x3D;&#x3D; &quot;string&quot;) before &#x3D; new Set([item.before]);</span><br><span class="line">	else if (Array.isArray(item.before)) &#123;</span><br><span class="line">		before &#x3D; new Set(item.before);</span><br><span class="line">	&#125;</span><br><span class="line">	let stage &#x3D; 0;</span><br><span class="line">	if (typeof item.stage &#x3D;&#x3D;&#x3D; &quot;number&quot;) stage &#x3D; item.stage;</span><br><span class="line">	let i &#x3D; this.taps.length;</span><br><span class="line">	while (i &gt; 0) &#123;</span><br><span class="line">		i--;</span><br><span class="line">		const x &#x3D; this.taps[i];</span><br><span class="line">		this.taps[i + 1] &#x3D; x;</span><br><span class="line">		const xStage &#x3D; x.stage || 0;</span><br><span class="line">		if (before) &#123;</span><br><span class="line">			if (before.has(x.name)) &#123;</span><br><span class="line">				before.delete(x.name);</span><br><span class="line">				continue;</span><br><span class="line">			&#125;</span><br><span class="line">			if (before.size &gt; 0) &#123;</span><br><span class="line">				continue;</span><br><span class="line">			&#125;</span><br><span class="line">		&#125;</span><br><span class="line">		if (xStage &gt; stage) &#123;</span><br><span class="line">			continue;</span><br><span class="line">		&#125;</span><br><span class="line">		i++;</span><br><span class="line">		break;</span><br><span class="line">	&#125;</span><br><span class="line">	this.taps[i] &#x3D; item;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>_insert主要是排序tap并放入到taps数组里面， 排序的算法并不是特别复杂，这里就不赘述了， 到了这里， 注册阶段就已经结束了， 继续看触发阶段。</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">hook.call(1, 2)  &#x2F;&#x2F; 触发函数</span><br></pre></td></tr></table></figure>

<p>在基类hook中， 有一个初始化过程，</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line">this.call &#x3D; this._call; </span><br><span class="line"></span><br><span class="line">Object.defineProperties(Hook.prototype, &#123;</span><br><span class="line">	_call: &#123;</span><br><span class="line">		value: createCompileDelegate(&quot;call&quot;, &quot;sync&quot;),</span><br><span class="line">		configurable: true,</span><br><span class="line">		writable: true</span><br><span class="line">	&#125;,</span><br><span class="line">	_promise: &#123;</span><br><span class="line">		value: createCompileDelegate(&quot;promise&quot;, &quot;promise&quot;),</span><br><span class="line">		configurable: true,</span><br><span class="line">		writable: true</span><br><span class="line">	&#125;,</span><br><span class="line">	_callAsync: &#123;</span><br><span class="line">		value: createCompileDelegate(&quot;callAsync&quot;, &quot;async&quot;),</span><br><span class="line">		configurable: true,</span><br><span class="line">		writable: true</span><br><span class="line">	&#125;</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure>

<p>我们可以看出_call是由createCompileDelegate生成的， 往下看</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">function createCompileDelegate(name, type) &#123;</span><br><span class="line">	return function lazyCompileHook(...args) &#123;</span><br><span class="line">		this[name] &#x3D; this._createCall(type);</span><br><span class="line">		return this[name](...args);</span><br><span class="line">	&#125;;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>createCompileDelegate返回一个名为lazyCompileHook的函数，顾名思义，即懒编译， 直到调用call的时候， 才会编译出正在的call函数。</p>
<p>createCompileDelegate也是调用的_createCall， 而_createCall调用了Compier函数</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">_createCall(type) &#123;</span><br><span class="line">	return this.compile(&#123;</span><br><span class="line">		taps: this.taps,</span><br><span class="line">		interceptors: this.interceptors,</span><br><span class="line">		args: this._args,</span><br><span class="line">		type: type</span><br><span class="line">	&#125;);</span><br><span class="line">&#125;  </span><br><span class="line">compile(options) &#123;</span><br><span class="line">	throw new Error(&quot;Abstract: should be overriden&quot;);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>可以看到compiler必须由子类重写， 返回到syncHook的compile函数， 即我们一开始说的核心方法</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line">class SyncHookCodeFactory extends HookCodeFactory &#123;</span><br><span class="line">	content(&#123; onError, onResult, onDone, rethrowIfPossible &#125;) &#123;</span><br><span class="line">		return this.callTapsSeries(&#123;</span><br><span class="line">			onError: (i, err) &#x3D;&gt; onError(err),</span><br><span class="line">			onDone,</span><br><span class="line">			rethrowIfPossible</span><br><span class="line">		&#125;);</span><br><span class="line">	&#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">const factory &#x3D; new SyncHookCodeFactory();</span><br><span class="line"></span><br><span class="line">class SyncHook extends Hook &#123;</span><br><span class="line">    ...</span><br><span class="line">	compile(options) &#123;</span><br><span class="line">		factory.setup(this, options);</span><br><span class="line">		return factory.create(options);</span><br><span class="line">	&#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>关键就在于SyncHookCodeFactory和工厂类HookCodeFactory，  先看setup函数，</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">setup(instance, options) &#123;</span><br><span class="line">  &#x2F;&#x2F; 这里的instance 是syncHook 实例, 其实就是把tap进来的钩子数组给到钩子的_x属性里.</span><br><span class="line">  instance._x &#x3D; options.taps.map(t &#x3D;&gt; t.fn);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>然后是最关键的create函数， 可以看到最后返回的fn，其实是一个new Function动态生成的函数</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br></pre></td><td class="code"><pre><span class="line">create(options) &#123;</span><br><span class="line">  &#x2F;&#x2F; 初始化参数,保存options到本对象this.options,保存new Hook([&quot;options&quot;]) 传入的参数到 this._args</span><br><span class="line">  this.init(options);</span><br><span class="line">  let fn;</span><br><span class="line">  &#x2F;&#x2F; 动态构建钩子,这里是抽象层,分同步, 异步, promise</span><br><span class="line">  switch (this.options.type) &#123;</span><br><span class="line">    &#x2F;&#x2F; 先看同步</span><br><span class="line">    case &quot;sync&quot;:</span><br><span class="line">      &#x2F;&#x2F; 动态返回一个钩子函数</span><br><span class="line">      fn &#x3D; new Function(</span><br><span class="line">        &#x2F;&#x2F; 生成函数的参数,no before no after 返回参数字符串 xxx,xxx 在</span><br><span class="line">        &#x2F;&#x2F; 注意这里this.args返回的是一个字符串,</span><br><span class="line">        &#x2F;&#x2F; 在这个例子中是options</span><br><span class="line">        this.args(),</span><br><span class="line">        &#39;&quot;use strict&quot;;\n&#39; +</span><br><span class="line">          this.header() +</span><br><span class="line">          this.content(&#123;</span><br><span class="line">            onError: err &#x3D;&gt; &#96;throw $&#123;err&#125;;\n&#96;,</span><br><span class="line">            onResult: result &#x3D;&gt; &#96;return $&#123;result&#125;;\n&#96;,</span><br><span class="line">            onDone: () &#x3D;&gt; &quot;&quot;,</span><br><span class="line">            rethrowIfPossible: true</span><br><span class="line">          &#125;)</span><br><span class="line">      );</span><br><span class="line">      break;</span><br><span class="line">    case &quot;async&quot;:</span><br><span class="line">      fn &#x3D; new Function(</span><br><span class="line">        this.args(&#123;</span><br><span class="line">          after: &quot;_callback&quot;</span><br><span class="line">        &#125;),</span><br><span class="line">        &#39;&quot;use strict&quot;;\n&#39; +</span><br><span class="line">          this.header() +</span><br><span class="line">          &#x2F;&#x2F; 这个 content 调用的是子类类的 content 函数,</span><br><span class="line">          &#x2F;&#x2F; 参数由子类传,实际返回的是 this.callTapsSeries() 返回的类容</span><br><span class="line">          this.content(&#123;</span><br><span class="line">            onError: err &#x3D;&gt; &#96;_callback($&#123;err&#125;);\n&#96;,</span><br><span class="line">            onResult: result &#x3D;&gt; &#96;_callback(null, $&#123;result&#125;);\n&#96;,</span><br><span class="line">            onDone: () &#x3D;&gt; &quot;_callback();\n&quot;</span><br><span class="line">          &#125;)</span><br><span class="line">      );</span><br><span class="line">      break;</span><br><span class="line">    case &quot;promise&quot;:</span><br><span class="line">      let code &#x3D; &quot;&quot;;</span><br><span class="line">      code +&#x3D; &#39;&quot;use strict&quot;;\n&#39;;</span><br><span class="line">      code +&#x3D; &quot;return new Promise((_resolve, _reject) &#x3D;&gt; &#123;\n&quot;;</span><br><span class="line">      code +&#x3D; &quot;var _sync &#x3D; true;\n&quot;;</span><br><span class="line">      code +&#x3D; this.header();</span><br><span class="line">      code +&#x3D; this.content(&#123;</span><br><span class="line">        onError: err &#x3D;&gt; &#123;</span><br><span class="line">          let code &#x3D; &quot;&quot;;</span><br><span class="line">          code +&#x3D; &quot;if(_sync)\n&quot;;</span><br><span class="line">          code +&#x3D; &#96;_resolve(Promise.resolve().then(() &#x3D;&gt; &#123; throw $&#123;err&#125;; &#125;));\n&#96;;</span><br><span class="line">          code +&#x3D; &quot;else\n&quot;;</span><br><span class="line">          code +&#x3D; &#96;_reject($&#123;err&#125;);\n&#96;;</span><br><span class="line">          return code;</span><br><span class="line">        &#125;,</span><br><span class="line">        onResult: result &#x3D;&gt; &#96;_resolve($&#123;result&#125;);\n&#96;,</span><br><span class="line">        onDone: () &#x3D;&gt; &quot;_resolve();\n&quot;</span><br><span class="line">      &#125;);</span><br><span class="line">      code +&#x3D; &quot;_sync &#x3D; false;\n&quot;;</span><br><span class="line">      code +&#x3D; &quot;&#125;);\n&quot;;</span><br><span class="line">      fn &#x3D; new Function(this.args(), code);</span><br><span class="line">      break;</span><br><span class="line">  &#125;</span><br><span class="line">  &#x2F;&#x2F; 把刚才init赋的值初始化为undefined</span><br><span class="line">  &#x2F;&#x2F; this.options &#x3D; undefined;</span><br><span class="line">  &#x2F;&#x2F; this._args &#x3D; undefined;</span><br><span class="line">  this.deinit();</span><br><span class="line"></span><br><span class="line">  return fn;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>最后生成的代码大致如下， 参考<a target="_blank" rel="noopener" href="https://segmentfault.com/a/1190000017421077#articleHeader9">文章</a></p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br></pre></td><td class="code"><pre><span class="line">&quot;use strict&quot;;</span><br><span class="line">function (options) &#123;</span><br><span class="line">  var _context;</span><br><span class="line">  var _x &#x3D; this._x;</span><br><span class="line">  var _taps &#x3D; this.taps;</span><br><span class="line">  var _interterceptors &#x3D; this.interceptors;</span><br><span class="line">&#x2F;&#x2F; 我们只有一个拦截器所以下面的只会生成一个</span><br><span class="line">  _interceptors[0].call(options);</span><br><span class="line"></span><br><span class="line">  var _tap0 &#x3D; _taps[0];</span><br><span class="line">  _interceptors[0].tap(_tap0);</span><br><span class="line">  var _fn0 &#x3D; _x[0];</span><br><span class="line">  _fn0(options);</span><br><span class="line">  var _tap1 &#x3D; _taps[1];</span><br><span class="line">  _interceptors[1].tap(_tap1);</span><br><span class="line">  var _fn1 &#x3D; _x[1];</span><br><span class="line">  _fn1(options);</span><br><span class="line">  var _tap2 &#x3D; _taps[2];</span><br><span class="line">  _interceptors[2].tap(_tap2);</span><br><span class="line">  var _fn2 &#x3D; _x[2];</span><br><span class="line">  _fn2(options);</span><br><span class="line">  var _tap3 &#x3D; _taps[3];</span><br><span class="line">  _interceptors[3].tap(_tap3);</span><br><span class="line">  var _fn3 &#x3D; _x[3];</span><br><span class="line">  _fn3(options);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>ok， 以上就是Tapabled的机制， 然而本篇的主要对象其实是基于tapable实现的compile和compilation对象。不过由于他们都是基于tapable，所以介绍的篇幅相对短一点。</p>
<h2 id="7-3-compile"><a href="#7-3-compile" class="headerlink" title="7.3 compile"></a>7.3 compile</h2><h3 id="7-3-1-compile是什么"><a href="#7-3-1-compile是什么" class="headerlink" title="7.3.1 compile是什么"></a>7.3.1 compile是什么</h3><blockquote>
<p>compiler 对象代表了完整的 webpack 环境配置。这个对象在启动 webpack 时被一次性建立，并配置好所有可操作的设置，包括 options，loader 和 plugin。当在 webpack 环境中应用一个插件时，插件将收到此 compiler 对象的引用。可以使用 compiler 来访问 webpack 的主环境。</p>
</blockquote>
<p>也就是说， compile是webpack的整体环境。</p>
<h3 id="7-3-2-compile的内部实现"><a href="#7-3-2-compile的内部实现" class="headerlink" title="7.3.2 compile的内部实现"></a>7.3.2 compile的内部实现</h3><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line">class Compiler extends Tapable &#123;</span><br><span class="line">  constructor(context) &#123;</span><br><span class="line">    super();</span><br><span class="line">    this.hooks &#x3D; &#123;</span><br><span class="line">      &#x2F;** @type &#123;SyncBailHook&lt;Compilation&gt;&#125; *&#x2F;</span><br><span class="line">      shouldEmit: new SyncBailHook([&quot;compilation&quot;]),</span><br><span class="line">      &#x2F;** @type &#123;AsyncSeriesHook&lt;Stats&gt;&#125; *&#x2F;</span><br><span class="line">      done: new AsyncSeriesHook([&quot;stats&quot;]),</span><br><span class="line">      &#x2F;** @type &#123;AsyncSeriesHook&lt;&gt;&#125; *&#x2F;</span><br><span class="line">      additionalPass: new AsyncSeriesHook([]),</span><br><span class="line">      &#x2F;** @type &#123;AsyncSeriesHook&lt;Compiler&gt;&#125; *&#x2F;</span><br><span class="line">      ......</span><br><span class="line">      ......</span><br><span class="line">      some code</span><br><span class="line">    &#125;;</span><br><span class="line">    ......</span><br><span class="line">    ......</span><br><span class="line">    some code</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>可以看到， Compier继承了Tapable,  并且在实例上绑定了一个hook对象， 使得Compier的实例compier可以像这样使用</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line">compiler.hooks.compile.tapAsync(</span><br><span class="line">  &#39;afterCompile&#39;,</span><br><span class="line">  (compilation, callback) &#x3D;&gt; &#123;</span><br><span class="line">    console.log(&#39;This is an example plugin!&#39;);</span><br><span class="line">    console.log(&#39;Here’s the &#96;compilation&#96; object which represents a single build of assets:&#39;, compilation);</span><br><span class="line"></span><br><span class="line">    &#x2F;&#x2F; 使用 webpack 提供的 plugin API 操作构建结果</span><br><span class="line">    compilation.addModule(&#x2F;* ... *&#x2F;);</span><br><span class="line"></span><br><span class="line">    callback();</span><br><span class="line">  &#125;</span><br><span class="line">);</span><br></pre></td></tr></table></figure>

<h2 id="7-4-compilation"><a href="#7-4-compilation" class="headerlink" title="7.4 compilation"></a>7.4 compilation</h2><h3 id="7-4-1-什么是compilation"><a href="#7-4-1-什么是compilation" class="headerlink" title="7.4.1 什么是compilation"></a>7.4.1 什么是compilation</h3><blockquote>
<p>compilation 对象代表了一次资源版本构建。当运行 webpack 开发环境中间件时，每当检测到一个文件变化，就会创建一个新的 compilation，从而生成一组新的编译资源。一个 compilation 对象表现了当前的模块资源、编译生成资源、变化的文件、以及被跟踪依赖的状态信息。compilation 对象也提供了很多关键时机的回调，以供插件做自定义处理时选择使用。</p>
</blockquote>
<h3 id="7-4-2-compilation的实现"><a href="#7-4-2-compilation的实现" class="headerlink" title="7.4.2 compilation的实现"></a>7.4.2 compilation的实现</h3><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><span class="line">class Compilation extends Tapable &#123;</span><br><span class="line">	&#x2F;**</span><br><span class="line">	 * Creates an instance of Compilation.</span><br><span class="line">	 * @param &#123;Compiler&#125; compiler the compiler which created the compilation</span><br><span class="line">	 *&#x2F;</span><br><span class="line">	constructor(compiler) &#123;</span><br><span class="line">		super();</span><br><span class="line">		this.hooks &#x3D; &#123;</span><br><span class="line">			&#x2F;** @type &#123;SyncHook&lt;Module&gt;&#125; *&#x2F;</span><br><span class="line">			buildModule: new SyncHook([&quot;module&quot;]),</span><br><span class="line">			&#x2F;** @type &#123;SyncHook&lt;Module&gt;&#125; *&#x2F;</span><br><span class="line">			rebuildModule: new SyncHook([&quot;module&quot;]),</span><br><span class="line">			&#x2F;** @type &#123;SyncHook&lt;Module, Error&gt;&#125; *&#x2F;</span><br><span class="line">			failedModule: new SyncHook([&quot;module&quot;, &quot;error&quot;]),</span><br><span class="line">			&#x2F;** @type &#123;SyncHook&lt;Module&gt;&#125; *&#x2F;</span><br><span class="line">			succeedModule: new SyncHook([&quot;module&quot;]),</span><br><span class="line"></span><br><span class="line">			&#x2F;** @type &#123;SyncHook&lt;Dependency, string&gt;&#125; *&#x2F;</span><br><span class="line">			addEntry: new SyncHook([&quot;entry&quot;, &quot;name&quot;]),</span><br><span class="line">			&#x2F;** @type &#123;SyncHook&lt;Dependency, string, Error&gt;&#125; *&#x2F;</span><br><span class="line">		&#125;</span><br><span class="line">	&#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>具体参考上面提到的compiler实现。</p>
<h2 id="7-5-编写一个插件"><a href="#7-5-编写一个插件" class="headerlink" title="7.5 编写一个插件"></a>7.5 编写一个插件</h2><p>了解到tapable\compiler\compilation之后， 再来看插件的实现就不再一头雾水了<br> 以下代码源自<a target="_blank" rel="noopener" href="https://webpack.docschina.org/contribute/writing-a-plugin/">官方文档</a></p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line">class MyExampleWebpackPlugin &#123;</span><br><span class="line">  &#x2F;&#x2F; 定义 &#96;apply&#96; 方法</span><br><span class="line">  apply(compiler) &#123;</span><br><span class="line">    &#x2F;&#x2F; 指定要追加的事件钩子函数</span><br><span class="line">    compiler.hooks.compile.tapAsync(</span><br><span class="line">      &#39;afterCompile&#39;,</span><br><span class="line">      (compilation, callback) &#x3D;&gt; &#123;</span><br><span class="line">        console.log(&#39;This is an example plugin!&#39;);</span><br><span class="line">        console.log(&#39;Here’s the &#96;compilation&#96; object which represents a single build of assets:&#39;, compilation);</span><br><span class="line"></span><br><span class="line">        &#x2F;&#x2F; 使用 webpack 提供的 plugin API 操作构建结果</span><br><span class="line">        compilation.addModule(&#x2F;* ... *&#x2F;);</span><br><span class="line"></span><br><span class="line">        callback();</span><br><span class="line">      &#125;</span><br><span class="line">    );</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>可以看到其实就是在apply中传入一个Compiler实例， 然后基于该实例注册事件， compilation同理， 最后webpack会在各流程执行call方法。</p>
<h2 id="7-6-compiler和compilation一些比较重要的事件钩子"><a href="#7-6-compiler和compilation一些比较重要的事件钩子" class="headerlink" title="7.6 compiler和compilation一些比较重要的事件钩子"></a>7.6 compiler和compilation一些比较重要的事件钩子</h2><h3 id="7-6-1-compier"><a href="#7-6-1-compier" class="headerlink" title="7.6.1 compier"></a>7.6.1 <a target="_blank" rel="noopener" href="https://webpack.docschina.org/api/compiler-hooks/#failed">compier</a></h3><table>
<thead>
<tr>
<th>事件钩子</th>
<th>触发时机</th>
<th>参数</th>
<th>类型</th>
</tr>
</thead>
<tbody><tr>
<td>entry-option</td>
<td>初始化 option</td>
<td>-</td>
<td>SyncBailHook</td>
</tr>
<tr>
<td>run</td>
<td>开始编译</td>
<td>compiler</td>
<td>AsyncSeriesHook</td>
</tr>
<tr>
<td>compile</td>
<td>真正开始的编译，在创建 compilation 对象之前</td>
<td>compilation</td>
<td>SyncHook</td>
</tr>
<tr>
<td>compilation</td>
<td>生成好了 compilation 对象，可以操作这个对象啦</td>
<td>compilation</td>
<td>SyncHook</td>
</tr>
<tr>
<td>make</td>
<td>从 entry 开始递归分析依赖，准备对每个模块进行 build</td>
<td>compilation</td>
<td>AsyncParallelHook</td>
</tr>
<tr>
<td>after-compile</td>
<td>编译 build 过程结束</td>
<td>compilation</td>
<td>AsyncSeriesHook</td>
</tr>
<tr>
<td>emit</td>
<td>在将内存中 assets 内容写到磁盘文件夹之前</td>
<td>compilation</td>
<td>AsyncSeriesHook</td>
</tr>
<tr>
<td>after-emit</td>
<td>在将内存中 assets 内容写到磁盘文件夹之后</td>
<td>compilation</td>
<td>AsyncSeriesHook</td>
</tr>
<tr>
<td>done</td>
<td>完成所有的编译过程</td>
<td>stats</td>
<td>AsyncSeriesHook</td>
</tr>
<tr>
<td>failed</td>
<td>编译失败的时候</td>
<td>error</td>
<td>SyncHook</td>
</tr>
</tbody></table>
<h3 id="7-6-2-compilation"><a href="#7-6-2-compilation" class="headerlink" title="7.6.2 compilation"></a>7.6.2 <a target="_blank" rel="noopener" href="https://webpack.docschina.org/api/compilation-hooks/">compilation</a></h3><table>
<thead>
<tr>
<th>事件钩子</th>
<th>触发时机</th>
<th>参数</th>
<th>类型</th>
</tr>
</thead>
<tbody><tr>
<td>normal-module-loader</td>
<td>普通模块 loader，真正（一个接一个地）加载模块图(graph)中所有模块的函数。</td>
<td>loaderContext module</td>
<td>SyncHook</td>
</tr>
<tr>
<td>seal</td>
<td>编译(compilation)停止接收新模块时触发。</td>
<td>-</td>
<td>SyncHook</td>
</tr>
<tr>
<td>optimize</td>
<td>优化阶段开始时触发。</td>
<td>-</td>
<td>SyncHook</td>
</tr>
<tr>
<td>optimize-modules</td>
<td>模块的优化</td>
<td>modules</td>
<td>SyncBailHook</td>
</tr>
<tr>
<td>optimize-chunks</td>
<td>优化 chunk</td>
<td>chunks</td>
<td>SyncBailHook</td>
</tr>
<tr>
<td>additional-assets</td>
<td>为编译(compilation)创建附加资源(asset)。</td>
<td>-</td>
<td>AsyncSeriesHook</td>
</tr>
<tr>
<td>optimize-chunk-assets</td>
<td>优化所有 chunk 资源(asset)。</td>
<td>chunks</td>
<td>AsyncSeriesHook</td>
</tr>
<tr>
<td>optimize-assets</td>
<td>优化存储在 compilation.assets 中的所有资源(asset)</td>
<td>assets</td>
<td>AsyncSeriesHook</td>
</tr>
</tbody></table>
<h2 id="总结-1"><a href="#总结-1" class="headerlink" title="总结"></a>总结</h2><p>插件机制并不复杂，webpack也不复杂，复杂的是插件本身..<br> 另外， 本应该先写流程的， 流程只能后面补上了。</p>
<h2 id="引用"><a href="#引用" class="headerlink" title="引用"></a>引用</h2><p><a target="_blank" rel="noopener" href="https://segmentfault.com/a/1190000017421077#articleHeader9">不满足于只会使用系列: tapable</a><br> <a target="_blank" rel="noopener" href="https://juejin.im/post/6844903750729990152">webpack系列之二Tapable</a><br> <a target="_blank" rel="noopener" href="https://webpack.docschina.org/contribute/writing-a-plugin/">编写一个插件</a><br> <a target="_blank" rel="noopener" href="https://github.com/webpack/webpack/blob/master/lib/Compiler.js">Compiler</a><br> <a target="_blank" rel="noopener" href="https://github.com/webpack/webpack/blob/master/lib/Compilation.js">Compilation</a><br> <a target="_blank" rel="noopener" href="https://webpack.docschina.org/api/compilation-hooks/#optimizeassets">compiler和comnpilation钩子</a><br> <a target="_blank" rel="noopener" href="https://zoumiaojiang.com/article/what-is-real-webpack-plugin/">看清楚真正的 Webpack 插件</a></p>
<h1 id="8-webpack-loader"><a href="#8-webpack-loader" class="headerlink" title="8.webpack-loader"></a>8.webpack-loader</h1><h2 id="8-1-问题"><a href="#8-1-问题" class="headerlink" title="8.1 问题"></a>8.1 问题</h2><p>以加载 less 为例。</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">&#x2F;&#x2F; example.js</span><br><span class="line">require(&#39;.&#x2F;style.less&#39;);</span><br><span class="line">&#x2F;&#x2F; style.less</span><br><span class="line">@color: #000fff;</span><br><span class="line">.content &#123;</span><br><span class="line">    width: 50px;</span><br><span class="line">    height: 50px;</span><br><span class="line">    background-color: @color;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>按照官方文档，想要加载 less 文件，我们需要配置三个 loader：style-loader!css-loader!less-loader。</p>
<p><strong>该从什么地方着手研究呢？</strong> → 仔细观察最终生成的 <a target="_blank" rel="noopener" href="https://github.com/youngwind/fake-webpack/blob/master/examples/loader/output.js">output.js </a>，如下图所示。</p>
<p><a target="_blank" rel="noopener" href="https://cloud.githubusercontent.com/assets/8401872/23387388/18895ef0-fd97-11e6-9dd7-94628c322e8e.png"><img data-src="https://cloud.githubusercontent.com/assets/8401872/23387388/18895ef0-fd97-11e6-9dd7-94628c322e8e.png" alt="image"></a></p>
<p>由此我们进行以下思考：</p>
<ol>
<li><p>既然最终 css 代码会被插入到 head 标签中，那么一定是模块2在起作用。但是，项目中并不包含这部分代码，经过排查，发现源自于 node-modules/style-loader/addStyle.js ，也就是说，是由 style-loader 引入的。（后面我们再考察是如何引入的）</p>
</li>
<li><p>观察模块3，那应该是 less 代码经过 less-loader 的转换之后，再包装一层 module.exports，成为一个 JS module。</p>
</li>
<li><p>style-loader 和 less-loader 的作用已经明了，但是，css-loader 发挥什么作用呢？虽然我一直按照官方文档配置三个 loader，但我从未真正理解为什么需要 css-loader。后来我在 css-loader 的文档中找到了答案。</p>
<blockquote>
<p><a target="_blank" rel="noopener" href="https://github.com/import">@import</a> and url() are interpreted like import and will be resolved by the css-loader.</p>
</blockquote>
<p>来源：<a target="_blank" rel="noopener" href="https://github.com/webpack-contrib/css-loader#options">https://github.com/webpack-contrib/css-loader#options</a></p>
<p>既然如此，为了降低实现的难度，我们<strong>暂时不予考虑 import 和 url 的情况，也就无需实现 css-loader 了。</strong></p>
</li>
<li><p>观察模块1，<code>require(2)(require(3))</code>，很显然：”模块3的导出作为模块2的输入参数，执行模块2“，也就是说：“将模块3中的 css 代码插入到 head 标签中“。理解这个逻辑不难，难点在于：<strong>webpack 如何知道应该拼接成 <code>require(2)(require(3))</code>，而不是别的什么。也就说，如何控制拼接出 <code>require(2)(require(3))</code>？</strong></p>
</li>
</ol>
<h2 id="8-2-思路"><a href="#8-2-思路" class="headerlink" title="8.2 思路"></a>8.2 思路</h2><p>思路进行到这儿，似乎走不下去了。看来只分析 output.js 还不足以理清，那么，让我们更进一步，观察 depTree，如下图所示。（图片较大，请点击放大查看）<br><a target="_blank" rel="noopener" href="https://cloud.githubusercontent.com/assets/8401872/23388226/ebcea460-fd9b-11e6-8cb8-cb2bb779ae62.png"><img data-src="https://cloud.githubusercontent.com/assets/8401872/23388226/ebcea460-fd9b-11e6-8cb8-cb2bb779ae62.png" alt="image"></a></p>
<p>问题在于：<strong>为什么凭空多出来2个模块？到底是哪里起了作用呢？→ 我在 style-loader 的源码中找到了答案。</strong></p>
<h2 id="8-3-style-loader-的再-require"><a href="#8-3-style-loader-的再-require" class="headerlink" title="8.3 style-loader 的再 require"></a>8.3 style-loader 的再 require</h2><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line">&#x2F;&#x2F; style-loader&#x2F;index.js</span><br><span class="line">const path &#x3D; require(&#39;path&#39;);</span><br><span class="line">module.exports &#x3D; function (content) &#123;</span><br><span class="line">   &#x2F;&#x2F; content 的值为：&#x2F;Users&#x2F;youngwind&#x2F;www&#x2F;fake-webpack&#x2F;node_modules&#x2F;style-loader-fake&#x2F;index.js!&#x2F;Users&#x2F;youngwind&#x2F;www&#x2F;fake-webpack&#x2F;node_modules&#x2F;less-loader-fake&#x2F;index.js!&#x2F;Users&#x2F;youngwind&#x2F;www&#x2F;fake-webpack&#x2F;examples&#x2F;loader&#x2F;style.less</span><br><span class="line">    let loaderSign &#x3D; this.request.indexOf(&quot;!&quot;);</span><br><span class="line">    let rawCss &#x3D; this.request.substr(loaderSign);</span><br><span class="line">    &#x2F;&#x2F; rawCss 的值为：&#x2F;Users&#x2F;youngwind&#x2F;www&#x2F;fake-webpack&#x2F;node_modules&#x2F;less-loader-fake&#x2F;index.js!&#x2F;Users&#x2F;youngwind&#x2F;www&#x2F;fake-webpack&#x2F;examples&#x2F;loader&#x2F;style.less</span><br><span class="line">    return &quot;require(&quot; + JSON.stringify(path.join(__dirname, &#39;addStyle&#39;)) + &quot;)&quot; +</span><br><span class="line">        &quot;(require(&quot; + JSON.stringify(rawCss) + &quot;))&quot;;</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure>

<p>观察源码，我们发现：<strong>style-loader 返回的字符串里面又包含了2个 require，分别 require 了 addStyle 和 less-loader!style.less，由此，我们终于找到了突破口。→ loader 本质上是一个函数，输入参数是一个字符串，输出参数也是一个字符串。当然，输出的参数会被当成是 JS 代码，从而被 esprima 解析成 AST，触发进一步的依赖解析。</strong> 这就是多引入2个模块的原因。</p>
<h2 id="8-4-loaders-的拆解与运行"><a href="#8-4-loaders-的拆解与运行" class="headerlink" title="8.4 loaders 的拆解与运行"></a>8.4 loaders 的拆解与运行</h2><p>loaders 就像首尾相接的管道那样，<strong>从右到左</strong>地被依次运行。对应的代码如下：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br></pre></td><td class="code"><pre><span class="line">&#x2F;&#x2F; buildDep.js</span><br><span class="line">&#x2F;**</span><br><span class="line"> * 运算文件类型对应的 loaders，比如: less 文件对应 style-loader 和 less-loader</span><br><span class="line"> * 这些 loaders 本质上是一些处理字符串的函数,输入是一个字符串,输出是另一个字符串,从右到左串行执行。</span><br><span class="line"> * @param &#123;string&#125; request 相当于 filenamesWithLoader ,比如 &#x2F;Users&#x2F;youngwind&#x2F;www&#x2F;fake-webpack&#x2F;node_modules&#x2F;fake-style-loader&#x2F;index.js!&#x2F;Users&#x2F;youngwind&#x2F;www&#x2F;fake-webpack&#x2F;node_modules&#x2F;fake-less-loader&#x2F;index.js!&#x2F;Users&#x2F;youngwind&#x2F;www&#x2F;fake-webpack&#x2F;examples&#x2F;loader&#x2F;style.less</span><br><span class="line"> * @param &#123;array&#125; loaders 此类型文件对应的loaders</span><br><span class="line"> * @param &#123;string&#125; content 文件内容</span><br><span class="line"> * @param &#123;object&#125; options 选项</span><br><span class="line"> * @returns &#123;Promise&#125;</span><br><span class="line"> *&#x2F;</span><br><span class="line">function execLoaders(request, loaders, content, options) &#123;</span><br><span class="line">    return new Promise((resolve, reject) &#x3D;&gt; &#123;</span><br><span class="line">        &#x2F;&#x2F; 当所有 loader 都执行完了，输出最终的字符串</span><br><span class="line">        if (!loaders.length) &#123;</span><br><span class="line">            resolve(content);</span><br><span class="line">            return;</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        let loaderFunctions &#x3D; [];</span><br><span class="line">        loaders.forEach(loaderName &#x3D;&gt; &#123;</span><br><span class="line">            let loader &#x3D; require(loaderName);</span><br><span class="line">            &#x2F;&#x2F; 每个loader 本质上是一个函数</span><br><span class="line">            loaderFunctions.push(loader);</span><br><span class="line">        &#125;);</span><br><span class="line"></span><br><span class="line">        nextLoader(content);</span><br><span class="line"></span><br><span class="line">        &#x2F;***</span><br><span class="line">         * 调用下一个 loader</span><br><span class="line">         * @param &#123;string&#125; content 上一个loader的输出字符串</span><br><span class="line">         *&#x2F;</span><br><span class="line">        function nextLoader(content) &#123;</span><br><span class="line">            if (!loaderFunctions.length) &#123;</span><br><span class="line">                resolve(content);</span><br><span class="line">                return;</span><br><span class="line">            &#125;</span><br><span class="line">            &#x2F;&#x2F; 请注意: loader有同步和异步两种类型。对于异步loader,如 less-loader,</span><br><span class="line">            &#x2F;&#x2F; 需要执行 async() 和 callback(),以修改标志位和回传字符串</span><br><span class="line">            let async &#x3D; false;</span><br><span class="line">            let context &#x3D; &#123;</span><br><span class="line">                request,</span><br><span class="line">                async: () &#x3D;&gt; &#123;</span><br><span class="line">                    async &#x3D; true;</span><br><span class="line">                &#125;,</span><br><span class="line">                callback: (content) &#x3D;&gt; &#123;</span><br><span class="line">                    nextLoader(content);</span><br><span class="line">                &#125;</span><br><span class="line">            &#125;;</span><br><span class="line"></span><br><span class="line">            &#x2F;&#x2F; 就是在这儿逐个调用 loader</span><br><span class="line">            let ret &#x3D; loaderFunctions.pop().call(context, content);</span><br><span class="line">            if(!async) &#123;</span><br><span class="line">                &#x2F;&#x2F; 递归调用下一个 loader</span><br><span class="line">                nextLoader(ret);</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>请注意：<strong>loader 也是分为同步和异步两种的，比如 style-loader 是同步的（看源码就知道，直接 return）；而 less-loader 却是异步的，为什么呢？</strong></p>
<h2 id="8-5-异步的-less-loader"><a href="#8-5-异步的-less-loader" class="headerlink" title="8.5 异步的 less-loader"></a>8.5 异步的 less-loader</h2><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line">&#x2F;&#x2F; less-loader</span><br><span class="line">const less &#x3D; require(&#39;less&#39;);</span><br><span class="line"></span><br><span class="line">module.exports &#x3D; function (source) &#123;</span><br><span class="line">    &#x2F;&#x2F; 声明此 loader 是异步的</span><br><span class="line">    this.async();</span><br><span class="line">    let resultCb &#x3D; this.callback;</span><br><span class="line">    less.render(source, (e, output) &#x3D;&gt; &#123;</span><br><span class="line">        if (e) &#123;</span><br><span class="line">            throw &#96;less解析出现错误: $&#123;e&#125;, $&#123;e.stack&#125;&#96;;</span><br><span class="line">        &#125;</span><br><span class="line">        resultCb(&quot;module.exports &#x3D; &quot; + JSON.stringify(output.css));</span><br><span class="line">    &#125;);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>由代码我们可以看出：<strong>less-loader 本质上只是调用了 less 本身的 render 方法，由于 less.render 是异步的，less-loader 肯定也得异步，所以需要通过回调函数来获取其解析之后的 css 代码。</strong></p>
<h2 id="8-6-node-modules-的逐级查找"><a href="#8-6-node-modules-的逐级查找" class="headerlink" title="8.6 node-modules 的逐级查找"></a>8.6 node-modules 的逐级查找</h2><p>还差最后一点，我们就能完成 loader 机制了。<br>试想以下情景：<strong>webpack 检测到当前为 less 文件，需要找到 style-loader 和 less-loader 运行。但是，webpack 怎么知道这两个 loader 藏在哪个目录下面呢？他们可能藏在 example.js 所在目录的任意上层文件夹的 node-modules 中。</strong> 说到底，我们还是得实现之前提到过的 <strong>node-modules 的逐级查找功能。</strong> 核心代码如下：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br></pre></td><td class="code"><pre><span class="line">&#x2F;&#x2F; resolve.js</span><br><span class="line">&#x2F;**</span><br><span class="line"> * 根据 loaders &#x2F; 模块名,生成待查找的路径集合</span><br><span class="line"> * @param &#123;string&#125; context 入口文件所在目录</span><br><span class="line"> * @param &#123;array&#125; identifiers 可能是loader的集合,也可能是模块名</span><br><span class="line"> * @returns &#123;Array&#125;</span><br><span class="line"> *&#x2F;</span><br><span class="line">function generateDirs(context, identifiers) &#123;</span><br><span class="line">    let dirs &#x3D; [];</span><br><span class="line">    for (let identifier of identifiers) &#123;</span><br><span class="line">        if (path.isAbsolute(identifier)) &#123;</span><br><span class="line">            &#x2F;&#x2F; 绝对路径</span><br><span class="line">            if (!path.extname(identifier)) &#123;</span><br><span class="line">                identifier +&#x3D; &#39;.js&#39;;</span><br><span class="line">            &#125;</span><br><span class="line">            dirs.push(identifier);</span><br><span class="line">        &#125; else if (identifier.startsWith(&#39;.&#x2F;&#39;) || identifier.startsWith(&#39;..&#x2F;&#39;)) &#123;</span><br><span class="line">            &#x2F;&#x2F; 相对路径</span><br><span class="line">            dirs.push(path.resolve(context, identifier));</span><br><span class="line">        &#125; else &#123;</span><br><span class="line">            &#x2F;&#x2F; 模块名,需要逐级生成目录</span><br><span class="line">            let ext &#x3D; path.extname(identifier);</span><br><span class="line">            if (!ext) &#123;</span><br><span class="line">                ext &#x3D; &#39;.js&#39;;</span><br><span class="line">            &#125;</span><br><span class="line">            let paths &#x3D; context.split(path.sep);</span><br><span class="line">            let tempPaths &#x3D; paths.slice();</span><br><span class="line">            for (let folder of tempPaths) &#123;</span><br><span class="line">                let newContext &#x3D; paths.join(path.sep);</span><br><span class="line">                dirs.push(path.resolve(newContext, &#39;.&#x2F;node_modules&#39;, &#96;.&#x2F;$&#123;identifier&#125;-loader-fake&#96;, &#96;index$&#123;ext&#125;&#96;));</span><br><span class="line">                paths.pop();</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    return dirs;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>举个例子，对于 style-loader 来说，生成的查找路径集合如下：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">[</span><br><span class="line">  &quot;&#x2F;Users&#x2F;youngwind&#x2F;www&#x2F;fake-webpack&#x2F;examples&#x2F;loader&#x2F;node_modules&#x2F;style-loader-fake&#x2F;index.js&quot;,</span><br><span class="line">  &quot;&#x2F;Users&#x2F;youngwind&#x2F;www&#x2F;fake-webpack&#x2F;examples&#x2F;node_modules&#x2F;style-loader-fake&#x2F;index.js&quot;,</span><br><span class="line">  &quot;&#x2F;Users&#x2F;youngwind&#x2F;www&#x2F;fake-webpack&#x2F;node_modules&#x2F;style-loader-fake&#x2F;index.js&quot;,</span><br><span class="line">  &quot;&#x2F;Users&#x2F;youngwind&#x2F;www&#x2F;node_modules&#x2F;style-loader-fake&#x2F;index.js&quot;,</span><br><span class="line">  &quot;&#x2F;Users&#x2F;youngwind&#x2F;node_modules&#x2F;style-loader-fake&#x2F;index.js&quot;,</span><br><span class="line">  &quot;&#x2F;Users&#x2F;node_modules&#x2F;style-loader-fake&#x2F;index.js&quot;,</span><br><span class="line">]</span><br></pre></td></tr></table></figure>

<p>程序按照这个顺序依次查找，直到找到为止或者最终找不到抛出错误。</p>
<h2 id="后话"><a href="#后话" class="headerlink" title="后话"></a>后话</h2><p>至此，我们就完成了一个非常简单的 loader 机制，可以通过 style-loader 和 less-loader 处理加载 less 文件。当然，还有很多可以完善的地方，比如：</p>
<ol>
<li>实现 css-loader，以处理 import 和 url 的情况</li>
<li>给 loader 传递选项参数，以控制是否压缩代码等等特性</li>
<li>……</li>
</ol>
<h1 id="9-微前端-前端微服务"><a href="#9-微前端-前端微服务" class="headerlink" title="9.微前端(前端微服务)"></a>9.微前端(前端微服务)</h1><h2 id="前言-1"><a href="#前言-1" class="headerlink" title="前言"></a>前言</h2><p>想跳过技术细节直接看怎么实践的同学可以直接看最后一节。</p>
<p>目前社区有很多关于微前端架构的介绍，但大多停留在概念介绍的阶段。而本文会就某一个具体的类型场景，着重介绍微前端架构可以<strong>带来什么价值</strong>以及<strong>具体实践过程中需要关注的技术决策</strong>，并辅以具体代码，从而能真正意义上帮助你构建一个<strong>生产可用</strong>的微前端架构系统。</p>
<p>两个月前 Twitter 曾爆发过关于微前端的“热烈”讨论，参与大佬众多(Dan、Larkin 等)，对“事件”本身我们今天不做过多评论(后面可能会写篇文章来回顾一下)，有兴趣的同学可以通过这篇文章（<a target="_blank" rel="noopener" href="https://zendev.com/2019/06/17/microfrontends-good-bad-ugly.html%EF%BC%89%E4%BA%86%E8%A7%A3%E4%B8%80%E4%BA%8C%E3%80%82">https://zendev.com/2019/06/17/microfrontends-good-bad-ugly.html）了解一二。</a></p>
<h2 id="9-1-微前端的价值"><a href="#9-1-微前端的价值" class="headerlink" title="9.1 微前端的价值"></a>9.1 微前端的价值</h2><p>微前端架构具备以下几个核心价值：</p>
<p><strong>技术栈无关</strong>：主框架不限制接入应用的技术栈，子应用具备完全自主权</p>
<p><strong>独立开发、独立部署</strong>：子应用仓库独立，前后端可独立开发，部署完成后主框架自动完成同步更新</p>
<p><strong>独立运行时</strong>：每个子应用之间状态隔离，运行时状态不共享</p>
<p>微前端架构旨在解决单体应用在一个相对长的时间跨度下，由于参与的人员、团队的增多、变迁，从一个普通应用演变成一个巨石应用( Frontend Monolith )后，随之而来的应用不可维护的问题。这类问题在企业级 Web 应用中尤其常见。</p>
<h2 id="9-2-针对中后台应用的解决方案"><a href="#9-2-针对中后台应用的解决方案" class="headerlink" title="9.2 针对中后台应用的解决方案"></a>9.2 针对中后台应用的解决方案</h2><p><strong>中后台应用由于其应用生命周期长(动辄 3+ 年)等特点，最后演变成一个巨石应用的概率往往高于其他类型的 web 应用。而从技术实现角度，微前端架构解决方案大概分为两类场景：</strong></p>
<p><strong>单实例</strong>：即同一时刻，只有一个子应用被展示，子应用具备一个完整的应用生命周期。通常基于 url 的变化来做子应用的切换。</p>
<p><strong>多实例</strong>：同一时刻可展示多个子应用。通常使用 Web Components 方案来做子应用封装，子应用更像是一个业务组件而不是应用。</p>
<p>本文将着重介绍<strong>单实例场景</strong>下的微前端架构实践方案（基于 single-spa），因为这个场景更贴近大部分中后台应用。</p>
<h2 id="9-3-行业现状"><a href="#9-3-行业现状" class="headerlink" title="9.3 行业现状"></a>9.3 行业现状</h2><p>传统的云控制台应用，几乎都会面临业务快速发展之后，单体应用进化成巨石应用的问题。为了解决产品研发之间各种耦合的问题，大部分企业也都会有自己的解决方案。笔者于17年底，针对国内外几个著名的云产品控制台，做过这样一个技术调研：</p>
<p><img data-src="https://antcloud-cnhz02-athomeweb-01.oss-cn-hzfinance.aliyuncs.com/image/2019-08-26/1c4ee3d5-d535-4bb4-a511-2d0e1274519d.png" alt="img"></p>
<p>MPA 方案的优点在于 部署简单、各应用之间硬隔离，天生具备技术栈无关、独立开发、独立部署的特性。缺点则也很明显，应用之间切换会造成浏览器重刷，由于产品域名之间相互跳转，流程体验上会存在断点。</p>
<p>SPA 则天生具备体验上的优势，应用直接无刷新切换，能极大的保证多产品之间流程操作串联时的流程性。缺点则在于各应用技术栈之间是强耦合的。</p>
<p>那我们有没有可能将 MPA 和SPA 两者的优势结合起来，构建出一个相对完善的微前端架构方案呢？</p>
<p>jsconf china 2016 大会上，ucloud 的同学分享了他们的基于 angularjs 的方案（单页应用“联邦制”实践），里面提到的 “联邦制” 概念很贴切，可以认为是早期的基于耦合技术栈的微前端架构实践。</p>
<h2 id="9-4-微前端架构实践中的问题"><a href="#9-4-微前端架构实践中的问题" class="headerlink" title="9.4 微前端架构实践中的问题"></a>9.4 微前端架构实践中的问题</h2><p>可以发现，微前端架构的优势，正是 MPA 与 SPA 架构优势的合集。即保证应用具备独立开发权的同时，又有将它们整合到一起保证产品完整的流程体验的能力。</p>
<p>这样一套模式下，应用的架构就会变成：</p>
<p><img data-src="https://antcloud-cnhz02-athomeweb-01.oss-cn-hzfinance.aliyuncs.com/image/2019-08-26/00e4ae14-6a48-4118-a9d3-bdcbf2debadd.png" alt="img"></p>
<p>Stitching layer 作为主框架的核心成员，充当调度者的角色，由它来决定在不同的条件下激活不同的子应用。因此主框架的定位则仅仅是：<strong>导航路由 + 资源加载框架。</strong></p>
<p>而具体要实现这样一套架构，我们需要解决以下几个技术问题：</p>
<p><strong>路由系统及 FutureStat</strong></p>
<p>我们在一个实现了微前端内核的产品中，正常访问一个子应用的页面时，可能会有这样一个链路：</p>
<p><img data-src="https://antcloud-cnhz02-athomeweb-01.oss-cn-hzfinance.aliyuncs.com/image/2019-08-26/86e06958-dc1b-404a-9aaa-7b7a33c7bab8.png" alt="img"></p>
<p>由于我们的子应用都是 lazy load 的，当浏览器重新刷新时，主框架的资源会被重新加载，同时异步 load 子应用的静态资源，由于此时主应用的路由系统已经激活，但子应用的资源可能还没有完全加载完毕，从而导致路由注册表里发现没有能匹配子应用 /subApp/123/detail 的规则，这时候就会导致跳 NotFound 页或者直接路由报错。</p>
<p>这个问题在所有 lazy load 方式加载子应用的方案中都会碰到，早些年前 angularjs 社区把这个问题统一称之为 Future State。</p>
<p>解决的思路也很简单，我们需要设计这样一套路由机制：</p>
<p>主框架配置子应用的路由为subApp: { url: ‘/subApp/**’, entry:’./subApp.js’ }，则当浏览器的地址为 /subApp/abc 时，框架需要先加载 entry 资源，待 entry 资源加载完毕，确保子应用的路由系统注册进主框架之后后，再去由子应用的路由系统接管 url change 事件。同时在子应用路由切出时，主框架需要触发相应的destroy 事件，子应用在监听到该事件时，调用自己的卸载方法卸载应用，如 React 场景下 destroy = () =&gt; ReactDOM.unmountAtNode(container) 。</p>
<p>要实现这样一套机制，我们可以自己去劫持 url change 事件从而实现自己的路由系统，也可以基于社区已有的 ui router library，尤其是 react-router 在 v4 之后实现了 Dynamic Routing 能力，我们只需要复写一部分路由发现的逻辑即可。这里我们推荐直接选择社区比较完善的相关实践single-spa。</p>
<p><strong>App Entry</strong></p>
<p>解决了路由问题后，主框架与子应用集成的方式，也会成为一个需要重点关注的技术决策。</p>
<p><strong>1. 构建时组合 VS 运行时组合</strong></p>
<p>微前端架构模式下，子应用打包的方式，基本分为两种：</p>
<p><img data-src="https://antcloud-cnhz02-athomeweb-01.oss-cn-hzfinance.aliyuncs.com/image/2019-08-26/ce38b308-f0c6-402d-a9fa-30cdb5424706.png" alt="img"></p>
<p>两者的优缺点也很明显：</p>
<p><img data-src="https://antcloud-cnhz02-athomeweb-01.oss-cn-hzfinance.aliyuncs.com/image/2019-08-26/faaf3ace-15a2-4db9-875e-92b41035d6c8.png" alt="img"></p>
<p>很显然，要实现真正的技术栈无关跟独立部署两个核心目标，大部分场景下我们需要使用运行时加载子应用这种方案。</p>
<p><strong>2. JS Entry vs HTMLEntry</strong></p>
<p>在确定了运行时载入的方案后，另一个需要决策的点是，我们需要子应用提供什么形式的资源作为渲染入口？</p>
<p>JS Entry 的方式通常是子应用将资源打成一个entry script，比如 single-spa 的 example 中的方式。但这个方案的限制也颇多，如要求子应用的所有资源打包到一个 js bundle 里，包括 css、图片等资源。除了打出来的包可能体积庞大之外的问题之外，资源的并行加载等特性也无法利用上。</p>
<p>HTML Entry 则更加灵活，直接将子应用打出来 HTML作为入口，主框架可以通过 fetch html 的方式获取子应用的静态资源，同时将 HTML document 作为子节点塞到主框架的容器中。这样不仅可以极大的减少主应用的接入成本，子应用的开发方式及打包方式基本上也不需要调整，而且可以天然的解决子应用之间样式隔离的问题(后面提到)。想象一下这样一个场景：</p>
<p><img data-src="https://antcloud-cnhz02-athomeweb-01.oss-cn-hzfinance.aliyuncs.com/image/2019-08-26/da63a733-edca-4e57-a1cd-f9e4358c5609.png" alt="img"></p>
<p><img data-src="https://antcloud-cnhz02-athomeweb-01.oss-cn-hzfinance.aliyuncs.com/image/2019-08-26/73cc3732-55af-4d60-b70a-9f2d0b206726.png" alt="img"></p>
<p>如果是 JS Entry 方案，主框架需要在子应用加载之前构建好相应的容器节点(比如这里的 “#root” 节点)，不然子应用加载时会因为找不到 container 报错。但问题在于，主应用并不能保证子应用使用的容器节点为某一特定标记元素。而 HTML Entry 的方案则天然能解决这一问题，保留子应用完整的环境上下文，从而确保子应用有良好的开发体验。</p>
<p>HTML Entry 方案下，主框架注册子应用的方式则变成：</p>
<p><img data-src="https://antcloud-cnhz02-athomeweb-01.oss-cn-hzfinance.aliyuncs.com/image/2019-08-26/c6c206b5-bf52-4494-ad0b-b443f9abb467.png" alt="img"></p>
<p>本质上这里 HTML 充当的是应用静态资源表的角色，在某些场景下，我们也可以将 HTML Entry 的方案优化成 Config Entry，从而减少一次请求，如：</p>
<p><img data-src="https://antcloud-cnhz02-athomeweb-01.oss-cn-hzfinance.aliyuncs.com/image/2019-08-26/74436fb6-dad1-4af6-9318-db5e217622a3.png" alt="img"></p>
<p>总结一下：</p>
<p><img data-src="https://antcloud-cnhz02-athomeweb-01.oss-cn-hzfinance.aliyuncs.com/image/2019-08-26/ce4bd74e-eb49-4538-bfc0-8b639d294c24.png" alt="img"></p>
<p><strong>3. 模块导入</strong></p>
<p>微前端架构下，我们需要获取到子应用暴露出的一些钩子引用，如 bootstrap、mount、unmout 等(参考 single-spa)，从而能对接入应用有一个完整的生命周期控制。而由于子应用通常又有集成部署、独立部署两种模式同时支持的需求，使得我们只能选择 umd 这种兼容性的模块格式打包我们的子应用。如何在浏览器运行时获取远程脚本中导出的模块引用也是一个需要解决的问题。</p>
<p>通常我们第一反应的解法，也是最简单的解法就是与子应用与主框架之间约定好一个全局变量，把导出的钩子引用挂载到这个全局变量上，然后主应用从这里面取生命周期函数。</p>
<p>这个方案很好用，但是最大的问题是，主应用与子应用之间存在一种强约定的打包协议。那我们是否能找出一种松耦合的解决方案呢？</p>
<p>很简单，我们只需要走 umd 包格式中的 global export 方式获取子应用的导出即可，大体的思路是通过给 window变量打标记，记住每次最后添加的全局变量，这个变量一般就是应用 export 后挂载到 global 上的变量。实现方式可以参考 systemjs global import，这里不再赘述。</p>
<p><strong>应用隔离</strong></p>
<p>微前端架构方案中有两个非常关键的问题，有没有解决这两个问题将直接标志你的方案是否真的生产可用。比较遗憾的是此前社区在这个问题上的处理都会不约而同选择”绕道“的方式，比如通过主子应用之间的一些默认约定去规避冲突。而今天我们会尝试从纯技术角度，更智能的解决应用之间可能冲突的问题。</p>
<p><strong>1. 样式隔离</strong></p>
<p>由于微前端场景下，不同技术栈的子应用会被集成到同一个运行时中，所以我们必须在框架层确保各个子应用之间不会出现样式互相干扰的问题。</p>
<p><strong>Shadow DOM？</strong></p>
<p>针对 “Isolated Styles” 这个问题，如果不考虑浏览器兼容性，通常第一个浮现到我们脑海里的方案会是 Web Components。基于 Web Components 的 Shadow DOM 能力，我们可以将每个子应用包裹到一个 Shadow DOM 中，保证其运行时的样式的绝对隔离。</p>
<p>但 Shadow DOM 方案在工程实践中会碰到一个常见问题，比如我们这样去构建了一个在 Shadow DOM 里渲染的子应用：</p>
<p><img data-src="https://antcloud-cnhz02-athomeweb-01.oss-cn-hzfinance.aliyuncs.com/image/2019-08-26/24ba6c51-5e32-4f65-b700-1e527ed44dc3.png" alt="img"></p>
<p>由于子应用的样式作用域仅在 shadow 元素下，那么一旦子应用中出现运行时越界跑到外面构建 DOM 的场景，必定会导致构建出来的 DOM 无法应用子应用的样式的情况。</p>
<p>比如 sub-app 里调用了antd modal 组件，由于 modal 是动态挂载到document.body 的，而由于 Shadow DOM 的特性 antd 的样式只会在 shadow 这个作用域下生效，结果就是弹出框无法应用到 antd 的样式。解决的办法是把 antd 样式上浮一层，丢到主文档里，但这么做意味着子应用的样式直接泄露到主文档了。gg…</p>
<p><strong>CSS Module? BEM?</strong></p>
<p>社区通常的实践是通过约定 css 前缀的方式来避免样式冲突，即各个子应用使用特定的前缀来命名 class，或者直接基于 css module 方案写样式。对于一个全新的项目，这样当然是可行，但是通常微前端架构更多的目标是解决存量/遗产 应用的接入问题。很显然遗产应用通常是很难有动力做大幅改造的。</p>
<p>最主要的是，约定的方式有一个无法解决的问题，假如子应用中使用了三方的组件库，三方库在写入了大量的全局样式的同时又不支持定制化前缀？比如 a 应用引入了 antd 2.x，而b 应用引入了 antd 3.x，两个版本的 antd 都写入了全局的 .menu class ，但又彼此不兼容怎么办？</p>
<p><strong>Dynamic Stylesheet !</strong></p>
<p>解决方案其实很简单，我们只需要在应用切出/卸载后，同时卸载掉其样式表即可，原理是浏览器会对所有的样式表的插入、移除做整个 CSSOM 的重构，从而达到 插入、卸载 样式的目的。这样即能保证，在一个时间点里，只有一个应用的样式表是生效的。</p>
<p>上文提到的 HTML Entry 方案则天生具备样式隔离的特性，因为应用卸载后会直接移除去 HTML 结构，从而自动移除了其样式表。</p>
<p>比如 HTML Entry 模式下，子应用加载完成的后的 DOM 结构可能长这样：</p>
<p><img data-src="https://antcloud-cnhz02-athomeweb-01.oss-cn-hzfinance.aliyuncs.com/image/2019-08-26/cf7eb534-e522-4da7-84e5-6c314f9d52a3.png" alt="img"></p>
<p>当子应用被替换或卸载时，subApp节点的innerHTML 也会被复写，//alipay.com/subapp.css 也就自然被移除样式也随之卸载了。</p>
<p><strong>2. JS 隔离</strong></p>
<p>解决了样式隔离的问题后，有一个更关键的问题我们还没有解决：如何确保各个子应用之间的全局变量不会互相干扰，从而保证每个子应用之间的软隔离？</p>
<p>这个问题比样式隔离的问题更棘手，社区的普遍玩法是给一些全局副作用加各种前缀从而避免冲突。但其实我们都明白，这种通过团队间的“口头”约定的方式往往低效且易碎，所有依赖人为约束的方案都很难避免由于人的疏忽导致的线上 bug。那么我们是否有可能打造出一个好用的且完全无约束的 JS 隔离方案呢？</p>
<p>针对 JS 隔离的问题，我们独创了一个运行时的 JS 沙箱。简单画了个架构图：</p>
<p><img data-src="https://antcloud-cnhz02-athomeweb-01.oss-cn-hzfinance.aliyuncs.com/image/2019-08-26/f9b794e2-24cc-40c8-bd57-7254c93d5908.png" alt="img"></p>
<p>即在应用的 bootstrap 及 mount 两个生命周期开始之前分别给全局状态打下快照，然后当应用切出/卸载时，将状态回滚至 bootstrap 开始之前的阶段，确保应用对全局状态的污染全部清零。而当应用二次进入时则再恢复至 mount 前的状态的，从而确保应用在 remount 时拥有跟第一次 mount 时一致的全局上下文。</p>
<p>当然沙箱里做的事情还远不止这些，其他的还包括一些对全局事件监听的劫持等，以确保应用在切出之后，对全局事件的监听能得到完整的卸载，同时也会在 remount 时重新监听这些全局事件，从而模拟出与应用独立运行时一致的沙箱环境。</p>
<h2 id="9-5-蚂蚁金服微前端落地实践"><a href="#9-5-蚂蚁金服微前端落地实践" class="headerlink" title="9.5 蚂蚁金服微前端落地实践"></a>9.5 蚂蚁金服微前端落地实践</h2><p>自去年年底伊始，我们便尝试基于微前端架构模式，构建出一套全链路的面向中后台场景的产品接入平台，目的是解决不同产品之间集成困难、流程割裂的问题，希望接入平台后的应用，不论使用哪种技术栈，在运行时都可以通过自定义配置，实现不同应用之间页面级别的自由组合，从而生成一个千人千面的个性化控制台。</p>
<p>目前这套平台已在蚂蚁生产环境运行半年多，同时接入了多个产品线的 40+ 应用、4+ 不同类型的技术栈。过程中针对大量微前端实践中的问题，我们总结出了一套完整的解决方案：</p>
<p><img data-src="https://antcloud-cnhz02-athomeweb-01.oss-cn-hzfinance.aliyuncs.com/image/2019-08-26/f44b35de-1d21-45dd-9998-e65cc52c0266.png" alt="img"></p>
<p>在内部得到充分的技术验证和线上考验之后，我们决定将这套解决方案开源出来！</p>
<p>qiankun - 一套完整的微前端解决方案</p>
<p><a target="_blank" rel="noopener" href="https://github.com/umijs/qiankun">https://github.com/umijs/qiankun</a></p>
<p>取名 qiankun，意为统一。我们希望通过 qiankun 这种技术手段，让你能很方便的将一个巨石应用改造成一个基于微前端架构的系统，并且不再需要去关注各种过程中的技术细节，做到真正的开箱即用和生产可用。</p>
<p>对于umi用户我们也提供了配套的qiankun插件，以便于 umi 应用能几乎零成本的接入 qiankun：</p>
<p>@umijs/plugin-qiankun</p>
<p><a target="_blank" rel="noopener" href="https://github.com/umijs/umi-plugin-qiankun/">https://github.com/umijs/umi-plugin-qiankun/</a></p>
<p>最后欢迎大家点赞使用提出宝贵的意见。</p>
<p>Maybe the most complete micro-frontends solution youever met.</p>
<p>可能是你见过的最完善的微前端架构解决方案。</p>
<h1 id="10-Nodejs模块机制"><a href="#10-Nodejs模块机制" class="headerlink" title="10.Nodejs模块机制"></a>10.Nodejs模块机制</h1><p>我们都知道Nodejs遵循的是<code>CommonJS</code>规范，当我们<code>require(&#39;moduleA&#39;)</code>时，模块是怎么通过名字或者路径获取到模块的呢？首先要聊一下模块引用、模块定义、模块标识三个概念。</p>
<h2 id="10-1-CommonJS规范"><a href="#10-1-CommonJS规范" class="headerlink" title="10.1 CommonJS规范"></a>10.1 CommonJS规范</h2><h3 id="10-1-1-模块引用"><a href="#10-1-1-模块引用" class="headerlink" title="10.1.1 模块引用"></a>10.1.1 模块引用</h3><p>模块上下文提供<code>require()</code>方法来引入外部模块，看似简单的require函数， 其实内部做了大量工作。示例代码如下：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">&#x2F;&#x2F; test.js</span><br><span class="line">&#x2F;&#x2F; 引入一个模块到当前上下文中</span><br><span class="line">const math &#x3D; require(&#39;math&#39;);</span><br><span class="line">math.add(1, 2);</span><br></pre></td></tr></table></figure>

<h3 id="10-1-2-模块定义"><a href="#10-1-2-模块定义" class="headerlink" title="10.1.2 模块定义"></a>10.1.2 模块定义</h3><p>模块上下文提供了<code>exports</code>对象用于导入导出当前模块的方法或者变量，并且它是唯一的导出出口。模块中存在一个<code>module</code>对象，它代表模块自身，<code>exports</code>是module的属性。<strong>一个文件就是一个模块</strong>，将方法作为属性挂载在exports上就可以定义导出的方式：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">&#x2F;&#x2F;math.js</span><br><span class="line">exports.add &#x3D; function () &#123;</span><br><span class="line">    let sum &#x3D; 0, i &#x3D; 0, args &#x3D; arguments, l &#x3D; args.length;</span><br><span class="line">    while(i &lt; l) &#123;</span><br><span class="line">        sum +&#x3D; args[i++];</span><br><span class="line">    &#125;</span><br><span class="line">    return sum;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>这样就可像<code>test.js</code>里那样在require()之后调用模块的属性或者方法了。</p>
<h3 id="10-1-3-模块标识"><a href="#10-1-3-模块标识" class="headerlink" title="10.1.3 模块标识"></a>10.1.3 模块标识</h3><p>模块标识就是传递<code>给require()</code>方法的参数，它必须是符合小驼峰命名的字符串，或者以<code>.</code>、<code>..</code>开头的相对路径或者绝对路径，可以没有文件后缀名<code>.js</code>.</p>
<h2 id="10-2-Node的模块实现"><a href="#10-2-Node的模块实现" class="headerlink" title="10.2 Node的模块实现"></a>10.2 Node的模块实现</h2><p>在Node中引入模块，需要经历如下四个步骤:</p>
<ul>
<li>路径分析</li>
<li>文件定位</li>
<li>编译执行</li>
<li>加入内存</li>
</ul>
<h3 id="10-2-1-路径分析"><a href="#10-2-1-路径分析" class="headerlink" title="10.2.1 路径分析"></a>10.2.1 路径分析</h3><p>Node.js中模块可以通过文件路径或名字获取模块的引用。<strong>模块的引用会映射到一个js文件路径</strong>。 在Node中模块分为两类：</p>
<ul>
<li>一是Node提供的模块，称为<strong>核心模块</strong>（内置模块），内置模块公开了一些常用的API给开发者，并且它们在Node进程开始的时候就预加载了。</li>
<li>另一类是用户编写的模块，称为<strong>文件模块</strong>。如通过NPM安装的第三方模块（third-party modules）或本地模块（local modules），每个模块都会暴露一个公开的API。以便开发者可以导入。如</li>
</ul>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">const mod &#x3D; require(&#39;module_name&#39;)</span><br><span class="line">const &#123; methodA &#125; &#x3D; require(&#39;module_name&#39;)</span><br></pre></td></tr></table></figure>

<p>执行后，Node内部会载入内置模块或通过NPM安装的模块。require函数会返回一个对象，该对象公开的API可能是函数、对象或者属性如函数、数组甚至任意类型的JS对象。</p>
<p>核心模块是Node源码在编译过程中编译进了二进制执行文件。在Node启动时这些模块就被加载进内存中，所以核心模块引入时省去了文件定位和编译执行两个步骤，并且在路径分析中优先判断，因此核心模块的加载速度是最快的。文件模块则是在运行时动态加载，速度比核心模块慢。</p>
<p>这里列下node模块的载入及缓存机制：</p>
<p>1、载入内置模块（A Core Module）</p>
<p>2、载入文件模块（A File Module）</p>
<p>3、载入文件目录模块（A Folder Module）</p>
<p>4、载入node_modules里的模块</p>
<p>5、自动缓存已载入模块</p>
<p><strong>1、载入内置模块</strong></p>
<p>Node的内置模块被编译为二进制形式，引用时直接使用名字而非文件路径。当第三方的模块和内置模块同名时，内置模块将覆盖第三方同名模块。因此命名时需要注意不要和内置模块同名。如获取一个http模块</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">const http &#x3D; require(&#39;http&#39;)</span><br></pre></td></tr></table></figure>

<p>返回的http即是实现了HTTP功能Node的内置模块。</p>
<p><strong>2、载入文件模块</strong></p>
<p>绝对路径的</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">const myMod &#x3D; require(&#39;&#x2F;home&#x2F;base&#x2F;my_mod&#39;)</span><br></pre></td></tr></table></figure>

<p>或相对路径的</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">const myMod &#x3D; require(&#39;.&#x2F;my_mod&#39;)</span><br></pre></td></tr></table></figure>

<p>注意，这里忽略了扩展名<code>.js</code>，以下是对等的</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">const myMod &#x3D; require(&#39;.&#x2F;my_mod&#39;)</span><br><span class="line">const myMod &#x3D; require(&#39;.&#x2F;my_mod.js&#39;)</span><br></pre></td></tr></table></figure>

<p><strong>3、载入文件目录模块</strong></p>
<p>可以直接require一个目录，假设有一个目录名为folder，如</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">const myMod &#x3D; require(&#39;.&#x2F;folder&#39;)</span><br></pre></td></tr></table></figure>

<p>此时，Node将搜索整个folder目录，Node会假设folder为一个包并试图找到包定义文件package.json。如果folder目录里没有包含<code>package.json</code>文件，Node会假设默认主文件为<code>index.js</code>，即会加载<code>index.js</code>。如果<code>index.js</code>也不存在， 那么加载将失败。</p>
<p><strong>4、载入node_modules里的模块</strong></p>
<p>如果模块名不是路径，也不是内置模块，Node将试图去当前目录的<code>node_modules</code>文件夹里搜索。如果当前目录的<code>node_modules</code>里没有找到，Node会从父目录的<code>node_modules</code>里搜索，这样递归下去直到根目录。</p>
<p><strong>5、自动缓存已载入模块</strong></p>
<p>对于已加载的模块Node会缓存下来，而不必每次都重新搜索。下面是一个示例</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">&#x2F;&#x2F; modA.js</span><br><span class="line">console.log(&#39;模块modA开始加载...&#39;)</span><br><span class="line">exports &#x3D; function() &#123;</span><br><span class="line">    console.log(&#39;Hi&#39;)</span><br><span class="line">&#125;</span><br><span class="line">console.log(&#39;模块modA加载完毕&#39;)</span><br><span class="line"></span><br><span class="line">&#x2F;&#x2F; init.js</span><br><span class="line">var mod1 &#x3D; require(&#39;.&#x2F;modA&#39;)</span><br><span class="line">var mod2 &#x3D; require(&#39;.&#x2F;modA&#39;)</span><br><span class="line">console.log(mod1 &#x3D;&#x3D;&#x3D; mod2)</span><br></pre></td></tr></table></figure>

<p>命令行<code>node init.js</code>执行：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">模块modA开始加载...</span><br><span class="line">模块modA加载完毕</span><br><span class="line">true</span><br></pre></td></tr></table></figure>

<p>可以看到虽然require了两次，但modA.js仍然只执行了一次。mod1和mod2是相同的，即两个引用都指向了同一个模块对象。</p>
<p><strong>优先从缓存加载</strong></p>
<p>和浏览器会缓存静态js文件一样，Node也会对引入的模块进行缓存，不同的是，浏览器仅仅缓存文件，而nodejs缓存的是编译和执行后的对象（<strong>缓存内存</strong>） <code>require()</code>对相同模块的二次加载一律采用缓存优先的方式，这是第一优先级的，核心模块缓存检查先于文件模块的缓存检查。</p>
<p>基于这点：我们可以编写一个模块，用来记录长期存在的变量。例如：我可以编写一个记录接口访问数的模块：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">let count &#x3D; &#123;&#125;; &#x2F;&#x2F; 因模块是封闭的，这里实际上借用了js闭包的概念</span><br><span class="line">exports.count &#x3D; function(name)&#123;</span><br><span class="line">    if(count[name])&#123;</span><br><span class="line">    	count[name]++;</span><br><span class="line">    &#125;else&#123;</span><br><span class="line">    	count[name] &#x3D; 1;</span><br><span class="line">    &#125;</span><br><span class="line">    console.log(name + &#39;被访问了&#39; + count[name] + &#39;次。&#39;);</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure>

<p>我们在路由的 <code>action</code> 或 <code>controller</code>里这样引用:</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">let count &#x3D; require(&#39;count&#39;);</span><br><span class="line"></span><br><span class="line">export.index &#x3D; function(req, res)&#123;</span><br><span class="line">    count(&#39;index&#39;);</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure>

<p>以上便完成了对接口调用数的统计，但这只是个demo，因为数据存储在内存，服务器重启后便会清空。真正的计数器一定是要结合持久化存储器的。</p>
<p>在进入路径查找之前有必要描述一下<code>module path</code>这个Node.js中的概念。对于每一个被加载的文件模块，创建这个模块对象的时候，这个模块便会有一个paths属性，其值根据当前文件的路径 计算得到。我们创建<code>modulepath.js</code>这样一个文件，其内容为：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">&#x2F;&#x2F; modulepath.js</span><br><span class="line">console.log(module.paths);</span><br></pre></td></tr></table></figure>

<p>我们将其放到任意一个目录中执行node modulepath.js命令，将得到以下的输出结果。</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">[ &#39;&#x2F;home&#x2F;ikeepstudying&#x2F;research&#x2F;node_modules&#39;,</span><br><span class="line">&#39;&#x2F;home&#x2F;ikeepstudying&#x2F;node_modules&#39;,</span><br><span class="line">&#39;&#x2F;home&#x2F;node_modules&#39;,</span><br><span class="line">&#39;&#x2F;node_modules&#39; ]</span><br></pre></td></tr></table></figure>

<h3 id="10-2-2-文件定位"><a href="#10-2-2-文件定位" class="headerlink" title="10.2.2 文件定位"></a>10.2.2 文件定位</h3><p><strong>1.文件扩展名分析</strong></p>
<p>调用<code>require()</code>方法时若参数没有文件扩展名，Node会按<code>.js</code>、<code>.json</code>、<code>.node</code>的顺寻补足扩展名，依次尝试。</p>
<p>在尝试过程中，需要调用<strong>fs模块阻塞式</strong>地判断文件是否存在。因为Node的执行是单线程的，这是一个会引起性能问题的地方。如果是<code>.node</code>或者·.json·文件可以加上扩展名加快一点速度。另一个诀窍是：同步配合缓存。</p>
<p><strong>2.目录分析和包</strong></p>
<p><code>require()</code>分析文件扩展名后，可能没有查到对应文件，而是找到了一个目录，此时Node会将目录当作一个包来处理。</p>
<p>首先， Node在挡墙目录下查找<code>package.json</code>，通过<code>JSON.parse()</code>解析出包描述对象，从中取出main属性指定的文件名进行定位。若main属性指定文件名错误，或者没有<code>pachage.json</code>文件，Node会将index当作默认文件名。</p>
<p>简而言之，如果<code>require</code>绝对路径的文件，查找时不会去遍历每一个<code>node_modules</code>目录，其速度最快。其余流程如下：</p>
<p>1.从<code>module path</code>数组中取出第一个目录作为查找基准。</p>
<p>2.直接从目录中查找该文件，如果存在，则结束查找。如果不存在，则进行下一条查找。</p>
<p>3.尝试添加<code>.js</code>、<code>.json</code>、<code>.node</code>后缀后查找，如果存在文件，则结束查找。如果不存在，则进行下一条。</p>
<p>4.尝试将<code>require</code>的参数作为一个包来进行查找，读取目录下的<code>package.json</code>文件，取得<code>main</code>参数指定的文件。</p>
<p>5.尝试查找该文件，如果存在，则结束查找。如果不存在，则进行第3条查找。</p>
<p>6.如果继续失败，则取出<code>module path</code>数组中的下一个目录作为基准查找，循环第1至5个步骤。</p>
<p>7.如果继续失败，循环第1至6个步骤，直到module path中的最后一个值。</p>
<p>8.如果仍然失败，则抛出异常。</p>
<p>整个查找过程十分类似原型链的查找和作用域的查找。所幸Node.js对路径查找实现了缓存机制，否则由于每次判断路径都是同步阻塞式进行，会导致严重的性能消耗。</p>
<p>一旦加载成功就以模块的路径进行缓存</p>
<p><img data-src="https://user-gold-cdn.xitu.io/2019/12/25/16f38d1d79c552e0?imageView2/0/w/1280/h/960/format/webp/ignore-error/1" alt="img"></p>
<h3 id="10-2-3-模块编译"><a href="#10-2-3-模块编译" class="headerlink" title="10.2.3 模块编译"></a>10.2.3 模块编译</h3><p>每个模块文件模块都是一个对象，它的定义如下:</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">function Module(id, parent) &#123;</span><br><span class="line">    this.id &#x3D; id;</span><br><span class="line">    this.exports &#x3D; &#123;&#125;;</span><br><span class="line">    this.parent &#x3D; parent;</span><br><span class="line">    if(parent &amp;&amp; parent.children) &#123;</span><br><span class="line">        parent.children.push(this);</span><br><span class="line">    &#125;</span><br><span class="line">    this.filename &#x3D; null;</span><br><span class="line">    this.loaded &#x3D; false;</span><br><span class="line">    this.children &#x3D; [];</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>对于不同扩展名，其载入方法也有所不同：</p>
<ul>
<li><code>.js</code>通过fs模块同步读取文件后编译执行。</li>
<li><code>.node</code>这是C/C++编写的扩展文件，通过<code>dlopen()</code>方法加载最后编译生成的文件</li>
<li><code>.json</code>同过fs模块同步读取文件后，用<code>JSON.pares()</code>解析返回结果</li>
</ul>
<p>其他当作<code>.js</code></p>
<p>每一个编译成功的模块都会将其文件路径作为索引缓存在<code>Module._cache</code>对象上。</p>
<p><strong><code>json</code> 文件的编译</strong></p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">.json&#96;文件调用的方法如下:其实就是调用&#96;JSON.parse</span><br><span class="line">&#x2F;&#x2F; Native extension for .json</span><br><span class="line">Module._extensions[&#39;.json&#39;] &#x3D; function(module, filename) &#123;</span><br><span class="line">    var content &#x3D; NativeModule.require(&#39;fs&#39;).readFileSync(filename, &#39;utf-8&#39;);</span><br><span class="line">    try &#123;</span><br><span class="line">        module.exports &#x3D; JSON.parse(stripBOM(content));</span><br><span class="line">    &#125; catch(err) &#123;</span><br><span class="line">        err.message &#x3D; filename + &#39;：&#39; + err.message;</span><br><span class="line">        throw err;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p><code>Module._extensions</code>会被赋值给<code>require()</code>的<code>extensions</code>属性，所以可以用:<code>console.log(require.extensions)</code>;输出系统中已有的扩展加载方式。 当然也可以自己增加一些特殊的加载:</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">require.extensions[&#39;.txt&#39;] &#x3D; function()&#123;</span><br><span class="line">	&#x2F;&#x2F; code</span><br><span class="line">&#125;;。</span><br></pre></td></tr></table></figure>

<p>但是官方不鼓励通过这种方式自定义扩展名加载，而是期望先将其他语言或文件编译成JavaScript文件后再加载，这样的好处在于不讲烦琐的编译加载等过程引入Node的执行过程。</p>
<p><strong><code>js</code>模块的编译</strong> 在编译的过程中，Node对获取的javascript文件内容进行了头尾包装，将文件内容包装在一个function中：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">(function (exports, require, module, __filename, __dirname) &#123;</span><br><span class="line">    var math &#x3D; require(‘math‘);</span><br><span class="line">    exports.area &#x3D; function(radius) &#123;</span><br><span class="line">       return Math.PI * radius * radius;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;)</span><br></pre></td></tr></table></figure>

<p>包装之后的代码会通过vm原生模块的<code>runInThisContext()</code>方法执行（具有明确上下文，不污染全局），返回一个具体的function对象，最后传参执行，执行后返回<code>module.exports</code>.</p>
<p><strong>核心模块编译</strong></p>
<p>核心模块分为<code>C/C++</code>编写和JavaScript编写的两个部分，其中<code>C/C++</code>文件放在Node项目的src目录下，JavaScript文件放在lib目录下。</p>
<p>1.转存为C/C++代码</p>
<p>Node采用了V8附带的js2c.py工具，将所有内置的JavaScript代码转换成C++里的数组，生成node_natives.h头文件：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line">namespace node &#123;</span><br><span class="line">    const char node_native[] &#x3D; &#123; 47, 47, ..&#125;;</span><br><span class="line">    const char dgram_native[] &#x3D; &#123; 47, 47, ..&#125;;</span><br><span class="line">    const char console_native &#x3D; &#123; 47, 47, ..&#125;;</span><br><span class="line">    const char buffer_native &#x3D; &#123; 47, 47, ..&#125;;</span><br><span class="line">    const char querystring_native &#x3D; &#123; 47, 47, ..&#125;;</span><br><span class="line">    const char punycode_native &#x3D; &#123; 47, 47, ..&#125;;</span><br><span class="line">    ...</span><br><span class="line">    struct _native &#123;</span><br><span class="line">        const char* name;</span><br><span class="line">        const char* source;</span><br><span class="line">        size_t source_len;</span><br><span class="line">    &#125;</span><br><span class="line">    static const struct _native natives[] &#x3D; &#123;</span><br><span class="line">      &#123; &quot;node&quot;, node_native, sizeof(node_native)-1&#125;,</span><br><span class="line">      &#123; &quot;dgram&quot;, dgram_native, sizeof(dgram_native)-1&#125;,</span><br><span class="line">      ...</span><br><span class="line">    &#125;;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>在这个过程中，<strong>JavaScript代码以字符串形式存储在node命名空间中,是不可直接执行的</strong>。在启动Node进程时，js代码直接加载到内存中。在加载的过程中，js核心模块经历标识符分析后直接定位到内存中。</p>
<p>2.编译js核心模块</p>
<p>lib目录下的模块文件也在引入过程中经历了头尾包装的过程，然后才执行和导出了exports对象。与文件模块的区别在于：获取源代码的方式（核心模块从内存加载）和缓存执行结果的位置。</p>
<p>js核心模块源文件通过<code>process.binding(&#39;natives&#39;)</code>取出，编译成功的模块缓存到<code>NativeModule._cache</code>上。代码如下：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">function NativeModule() &#123;</span><br><span class="line">    this.filename &#x3D; id + &#39;.js&#39;;</span><br><span class="line">    this.id &#x3D; id;</span><br><span class="line">    this.exports &#x3D; &#123;&#125;;</span><br><span class="line">    this.loaded &#x3D; fales;</span><br><span class="line">&#125;</span><br><span class="line">NativeModule._source &#x3D; process.binding(&#39;natives&#39;);</span><br><span class="line">NativeModule._cache &#x3D; &#123;&#125;;</span><br></pre></td></tr></table></figure>

<h2 id="10-3-import和require"><a href="#10-3-import和require" class="headerlink" title="10.3 import和require"></a>10.3 <code>import</code>和<code>require</code></h2><p>简单的说一下<code>import</code>和<code>require</code>的本质区别</p>
<p><code>import</code>是ES6的模块规范，<code>require</code>是commonjs的模块规范，详细的用法我不介绍，我只想说一下他们最基本的区别，<strong>import是静态加载模块，require是动态加载</strong>，那么静态加载和动态加载的区别是什么呢？</p>
<p>静态加载时代码在编译的时候已经执行了，动态加载是编译后在代码运行的时候再执行，那么具体点是什么呢？ 先说说import，如下代码</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">import &#123; name &#125; from &#39;name.js&#39;</span><br><span class="line"></span><br><span class="line">&#x2F;&#x2F; name.js文件</span><br><span class="line">export let name &#x3D; &#39;jinux&#39;</span><br><span class="line">export let age &#x3D; 20</span><br></pre></td></tr></table></figure>

<p>上面的代码表示<code>main.js</code>文件里引入了<code>name.js</code>文件导出的变量，在代码编译阶段执行后的代码如下：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">let name &#x3D; &#39;jinux&#39;</span><br></pre></td></tr></table></figure>

<p>这个是我自己理解的，其实就是直接把<code>name.js</code>里的代码放到了<code>main.js</code>文件里，好比是在<code>main.js</code>文件中声明一样。 再来看看require</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">var obj &#x3D; require(&#39;obj.js&#39;);</span><br><span class="line"></span><br><span class="line">&#x2F;&#x2F; obj.js文件</span><br><span class="line">var obj &#x3D; &#123;</span><br><span class="line">  name: &#39;jinux&#39;,</span><br><span class="line">  age: 20</span><br><span class="line">&#125;</span><br><span class="line">module.export obj;</span><br></pre></td></tr></table></figure>

<p>require是在运行阶段，需要把obj对象整个加载进内存，之后用到哪个变量就用哪个，这里再对比一下<code>import</code>，<code>import</code>是静态加载，如果只引入了name，age是不会引入的，所以是按需引入，性能更好一点。</p>
<h2 id="10-4-nodejs清除require缓存"><a href="#10-4-nodejs清除require缓存" class="headerlink" title="10.4 nodejs清除require缓存"></a>10.4 nodejs清除require缓存</h2><p>开发nodejs应用时会面临一个麻烦的事情，就是修改了配置数据之后，必须重启服务器才能看到修改后的结果。</p>
<p>于是问题来了，挖掘机哪家强？噢，no! no! no!怎么做到修改文件之后，自动重启服务器。</p>
<p><code>server.js</code>中的片段：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">const port &#x3D; process.env.port || 1337;</span><br><span class="line">app.listen(port);</span><br><span class="line">console.log(&quot;server start in &quot; + port);</span><br><span class="line">exports.app &#x3D; app;</span><br></pre></td></tr></table></figure>

<p>假定我们现在是这样的, app.js的片段：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">const app &#x3D; require(&#39;.&#x2F;server.js&#39;);</span><br></pre></td></tr></table></figure>

<p>如果我们在server.js中启动了服务器，我们停止服务器可以在app.js中调用</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">app.app.close()</span><br><span class="line">复制代码</span><br></pre></td></tr></table></figure>

<p>但是当我们重新引入server.js</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">app &#x3D;  require(&#39;.&#x2F;server.js&#39;)</span><br></pre></td></tr></table></figure>

<p>的时候会发现并不是用的最新的server.js文件，原因是require的缓存机制，在第一次调用<code>require(&#39;./server.js&#39;)</code>的时候缓存下来了。</p>
<p>这个时候怎么办？</p>
<p>下面的代码解决了这个问题:</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">delete require.cache[require.resolve(&#39;.&#x2F;server.js&#39;)];</span><br><span class="line">app &#x3D; require(&#39;.&#x2F;server.js&#39;);</span><br></pre></td></tr></table></figure>

<h1 id="11-require原理"><a href="#11-require原理" class="headerlink" title="11.require原理"></a>11.require原理</h1><p>2009年，<a target="_blank" rel="noopener" href="http://nodejs.org/">Node.js</a> 项目诞生，所有模块一律为 <a target="_blank" rel="noopener" href="https://en.wikipedia.org/wiki/CommonJS">CommonJS</a> 格式。</p>
<p>时至今日，Node.js 的模块仓库 <a target="_blank" rel="noopener" href="https://www.npmjs.com/">npmjs.com</a> ，已经存放了15万个模块，其中绝大部分都是 CommonJS 格式。</p>
<p>这种格式的核心就是 require 语句，模块通过它加载。学习 Node.js ，必学如何使用 require 语句。本文通过源码分析，详细介绍 require 语句的内部运行机制，帮你理解 Node.js 的模块机制。</p>
<p><img data-src="http://www.ruanyifeng.com/blogimg/asset/2015/bg2015052001.png" alt="img"></p>
<h2 id="11-1-require-的基本用法"><a href="#11-1-require-的基本用法" class="headerlink" title="11.1 require() 的基本用法"></a>11.1 require() 的基本用法</h2><p>分析源码之前，先介绍 require 语句的内部逻辑。如果你只想了解 require 的用法，只看这一段就够了。</p>
<p>下面的内容翻译自<a target="_blank" rel="noopener" href="https://nodejs.org/api/modules.html#modules_all_together">《Node使用手册》</a>。</p>
<blockquote>
<p>当 Node 遇到 require(X) 时，按下面的顺序处理。</p>
<p>（1）如果 X 是内置模块（比如 require(‘http’）)<br>　　a. 返回该模块。<br>　　b. 不再继续执行。</p>
<p>（2）如果 X 以 “./“ 或者 “/“ 或者 “../“ 开头<br>　　a. 根据 X 所在的父模块，确定 X 的绝对路径。<br>　　b. 将 X 当成文件，依次查找下面文件，只要其中有一个存在，就返回该文件，不再继续执行。</p>
<blockquote>
<ul>
<li>X</li>
<li>X.js</li>
<li>X.json</li>
<li>X.node</li>
</ul>
</blockquote>
<p>　　c. 将 X 当成目录，依次查找下面文件，只要其中有一个存在，就返回该文件，不再继续执行。</p>
<blockquote>
<ul>
<li>X/package.json（main字段）</li>
<li>X/index.js</li>
<li>X/index.json</li>
<li>X/index.node</li>
</ul>
</blockquote>
<p>（3）如果 X 不带路径<br>　　a. 根据 X 所在的父模块，确定 X 可能的安装目录。<br>　　b. 依次在每个目录中，将 X 当成文件名或目录名加载。</p>
<p>（4） 抛出 “not found”</p>
</blockquote>
<p>请看一个例子。</p>
<p>当前脚本文件 /home/ry/projects/foo.js 执行了 require(‘bar’) ，这属于上面的第三种情况。Node 内部运行过程如下。</p>
<p>首先，确定 x 的绝对路径可能是下面这些位置，依次搜索每一个目录。</p>
<blockquote>
<figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">/home/ry/projects/node_modules/bar</span><br><span class="line">/home/ry/node_modules/bar</span><br><span class="line">/home/node_modules/bar</span><br><span class="line">/node_modules/bar</span><br></pre></td></tr></table></figure>
</blockquote>
<p>搜索时，Node 先将 bar 当成文件名，依次尝试加载下面这些文件，只要有一个成功就返回。</p>
<blockquote>
<figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">bar</span><br><span class="line">bar.js</span><br><span class="line">bar.json</span><br><span class="line">bar.node</span><br></pre></td></tr></table></figure>
</blockquote>
<p>如果都不成功，说明 bar 可能是目录名，于是依次尝试加载下面这些文件。</p>
<blockquote>
<figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">bar/package.json（main字段）</span><br><span class="line">bar/index.js</span><br><span class="line">bar/index.json</span><br><span class="line">bar/index.node</span><br></pre></td></tr></table></figure>
</blockquote>
<p>如果在所有目录中，都无法找到 bar 对应的文件或目录，就抛出一个错误。</p>
<h2 id="11-2-Module-构造函数"><a href="#11-2-Module-构造函数" class="headerlink" title="11.2 Module 构造函数"></a>11.2 Module 构造函数</h2><p>了解内部逻辑以后，下面就来看源码。</p>
<p>require 的源码在 Node 的 <a target="_blank" rel="noopener" href="https://github.com/joyent/node/blob/master/lib/module.js">lib/module.js</a> 文件。为了便于理解，本文引用的源码是简化过的，并且删除了原作者的注释。</p>
<blockquote>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">Module</span>(<span class="params">id, parent</span>) </span>&#123;</span><br><span class="line">  <span class="built_in">this</span>.id = id;</span><br><span class="line">  <span class="built_in">this</span>.exports = &#123;&#125;;</span><br><span class="line">  <span class="built_in">this</span>.parent = parent;</span><br><span class="line">  <span class="built_in">this</span>.filename = <span class="literal">null</span>;</span><br><span class="line">  <span class="built_in">this</span>.loaded = <span class="literal">false</span>;</span><br><span class="line">  <span class="built_in">this</span>.children = [];</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="built_in">module</span>.exports = Module;</span><br><span class="line"></span><br><span class="line"><span class="keyword">var</span> <span class="built_in">module</span> = <span class="keyword">new</span> Module(filename, parent);</span><br></pre></td></tr></table></figure>
</blockquote>
<p>上面代码中，Node 定义了一个构造函数 Module，所有的模块都是 Module 的实例。可以看到，当前模块（module.js）也是 Module 的一个实例。</p>
<p>每个实例都有自己的属性。下面通过一个例子，看看这些属性的值是什么。新建一个脚本文件 a.js 。</p>
<blockquote>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// a.js</span></span><br><span class="line"></span><br><span class="line"><span class="built_in">console</span>.log(<span class="string">&#x27;module.id: &#x27;</span>, <span class="built_in">module</span>.id);</span><br><span class="line"><span class="built_in">console</span>.log(<span class="string">&#x27;module.exports: &#x27;</span>, <span class="built_in">module</span>.exports);</span><br><span class="line"><span class="built_in">console</span>.log(<span class="string">&#x27;module.parent: &#x27;</span>, <span class="built_in">module</span>.parent);</span><br><span class="line"><span class="built_in">console</span>.log(<span class="string">&#x27;module.filename: &#x27;</span>, <span class="built_in">module</span>.filename);</span><br><span class="line"><span class="built_in">console</span>.log(<span class="string">&#x27;module.loaded: &#x27;</span>, <span class="built_in">module</span>.loaded);</span><br><span class="line"><span class="built_in">console</span>.log(<span class="string">&#x27;module.children: &#x27;</span>, <span class="built_in">module</span>.children);</span><br><span class="line"><span class="built_in">console</span>.log(<span class="string">&#x27;module.paths: &#x27;</span>, <span class="built_in">module</span>.paths);</span><br></pre></td></tr></table></figure>
</blockquote>
<p>运行这个脚本。</p>
<blockquote>
<figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line">$ node a.js</span><br><span class="line"></span><br><span class="line">module.id:  .</span><br><span class="line">module.exports:  &#123;&#125;</span><br><span class="line">module.parent:  null</span><br><span class="line">module.filename:  /home/ruanyf/tmp/a.js</span><br><span class="line">module.loaded:  <span class="literal">false</span></span><br><span class="line">module.children:  []</span><br><span class="line">module.paths:  [ <span class="string">&#x27;/home/ruanyf/tmp/node_modules&#x27;</span>,</span><br><span class="line">  <span class="string">&#x27;/home/ruanyf/node_modules&#x27;</span>,</span><br><span class="line">  <span class="string">&#x27;/home/node_modules&#x27;</span>,</span><br><span class="line">  <span class="string">&#x27;/node_modules&#x27;</span> ]</span><br></pre></td></tr></table></figure>
</blockquote>
<p>可以看到，如果没有父模块，直接调用当前模块，parent 属性就是 null，id 属性就是一个点。filename 属性是模块的绝对路径，path 属性是一个数组，包含了模块可能的位置。另外，输出这些内容时，模块还没有全部加载，所以 loaded 属性为 false 。</p>
<p>新建另一个脚本文件 b.js，让其调用 a.js 。</p>
<blockquote>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// b.js</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">var</span> a = <span class="built_in">require</span>(<span class="string">&#x27;./a.js&#x27;</span>);</span><br></pre></td></tr></table></figure>
</blockquote>
<p>运行 b.js 。</p>
<blockquote>
<figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line">$ node b.js</span><br><span class="line"></span><br><span class="line">module.id:  /home/ruanyf/tmp/a.js</span><br><span class="line">module.exports:  &#123;&#125;</span><br><span class="line">module.parent:  &#123; object &#125;</span><br><span class="line">module.filename:  /home/ruanyf/tmp/a.js</span><br><span class="line">module.loaded:  <span class="literal">false</span></span><br><span class="line">module.children:  []</span><br><span class="line">module.paths:  [ <span class="string">&#x27;/home/ruanyf/tmp/node_modules&#x27;</span>,</span><br><span class="line">  <span class="string">&#x27;/home/ruanyf/node_modules&#x27;</span>,</span><br><span class="line">  <span class="string">&#x27;/home/node_modules&#x27;</span>,</span><br><span class="line">  <span class="string">&#x27;/node_modules&#x27;</span> ]</span><br></pre></td></tr></table></figure>
</blockquote>
<p>上面代码中，由于 a.js 被 b.js 调用，所以 parent 属性指向 b.js 模块，id 属性和 filename 属性一致，都是模块的绝对路径。</p>
<h2 id="11-3-模块实例的-require-方法"><a href="#11-3-模块实例的-require-方法" class="headerlink" title="11.3 模块实例的 require 方法"></a>11.3 模块实例的 require 方法</h2><p>每个模块实例都有一个 require 方法。</p>
<blockquote>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">Module.prototype.require = <span class="function"><span class="keyword">function</span>(<span class="params">path</span>) </span>&#123;</span><br><span class="line">  <span class="keyword">return</span> Module._load(path, <span class="built_in">this</span>);</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure>
</blockquote>
<p>由此可知，require 并不是全局性命令，而是每个模块提供的一个内部方法，也就是说，只有在模块内部才能使用 require 命令（唯一的例外是 REPL 环境）。另外，require 其实内部调用 Module._load 方法。</p>
<p>下面来看 Module._load 的源码。</p>
<blockquote>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br></pre></td><td class="code"><pre><span class="line">Module._load = <span class="function"><span class="keyword">function</span>(<span class="params">request, parent, isMain</span>) </span>&#123;</span><br><span class="line"></span><br><span class="line">  <span class="comment">//  计算绝对路径</span></span><br><span class="line">  <span class="keyword">var</span> filename = Module._resolveFilename(request, parent);</span><br><span class="line"></span><br><span class="line">  <span class="comment">//  第一步：如果有缓存，取出缓存</span></span><br><span class="line">  <span class="keyword">var</span> cachedModule = Module._cache[filename];</span><br><span class="line">  <span class="keyword">if</span> (cachedModule) &#123;</span><br><span class="line">    <span class="keyword">return</span> cachedModule.exports;</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 第二步：是否为内置模块</span></span><br><span class="line">  <span class="keyword">if</span> (NativeModule.exists(filename)) &#123;</span><br><span class="line">    <span class="keyword">return</span> NativeModule.require(filename);</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 第三步：生成模块实例，存入缓存</span></span><br><span class="line">  <span class="keyword">var</span> <span class="built_in">module</span> = <span class="keyword">new</span> Module(filename, parent);</span><br><span class="line">  Module._cache[filename] = <span class="built_in">module</span>;</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 第四步：加载模块</span></span><br><span class="line">  <span class="keyword">try</span> &#123;</span><br><span class="line">    <span class="built_in">module</span>.load(filename);</span><br><span class="line">    hadException = <span class="literal">false</span>;</span><br><span class="line">  &#125; <span class="keyword">finally</span> &#123;</span><br><span class="line">    <span class="keyword">if</span> (hadException) &#123;</span><br><span class="line">      <span class="keyword">delete</span> Module._cache[filename];</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 第五步：输出模块的exports属性</span></span><br><span class="line">  <span class="keyword">return</span> <span class="built_in">module</span>.exports;</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure>
</blockquote>
<p>上面代码中，首先解析出模块的绝对路径（filename），以它作为模块的识别符。然后，如果模块已经在缓存中，就从缓存取出；如果不在缓存中，就加载模块。</p>
<p>因此，Module._load 的关键步骤是两个。</p>
<blockquote>
<ul>
<li>Module._resolveFilename() ：确定模块的绝对路径</li>
<li>module.load()：加载模块</li>
</ul>
</blockquote>
<h2 id="11-4-模块的绝对路径"><a href="#11-4-模块的绝对路径" class="headerlink" title="11.4 模块的绝对路径"></a>11.4 模块的绝对路径</h2><p>下面是 Module._resolveFilename 方法的源码。</p>
<blockquote>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line">Module._resolveFilename = <span class="function"><span class="keyword">function</span>(<span class="params">request, parent</span>) </span>&#123;</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 第一步：如果是内置模块，不含路径返回</span></span><br><span class="line">  <span class="keyword">if</span> (NativeModule.exists(request)) &#123;</span><br><span class="line">    <span class="keyword">return</span> request;</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 第二步：确定所有可能的路径</span></span><br><span class="line">  <span class="keyword">var</span> resolvedModule = Module._resolveLookupPaths(request, parent);</span><br><span class="line">  <span class="keyword">var</span> id = resolvedModule[<span class="number">0</span>];</span><br><span class="line">  <span class="keyword">var</span> paths = resolvedModule[<span class="number">1</span>];</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 第三步：确定哪一个路径为真</span></span><br><span class="line">  <span class="keyword">var</span> filename = Module._findPath(request, paths);</span><br><span class="line">  <span class="keyword">if</span> (!filename) &#123;</span><br><span class="line">    <span class="keyword">var</span> err = <span class="keyword">new</span> <span class="built_in">Error</span>(<span class="string">&quot;Cannot find module &#x27;&quot;</span> + request + <span class="string">&quot;&#x27;&quot;</span>);</span><br><span class="line">    err.code = <span class="string">&#x27;MODULE_NOT_FOUND&#x27;</span>;</span><br><span class="line">    <span class="keyword">throw</span> err;</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="keyword">return</span> filename;</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure>
</blockquote>
<p>上面代码中，在 Module.*resolveFilename 方法内部，又调用了两个方法 Module.*resolveLookupPaths() 和 Module._findPath() ，前者用来列出可能的路径，后者用来确认哪一个路径为真。</p>
<p>为了简洁起见，这里只给出 Module._resolveLookupPaths() 的运行结果。</p>
<blockquote>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">[   <span class="string">&#x27;/home/ruanyf/tmp/node_modules&#x27;</span>,</span><br><span class="line">    <span class="string">&#x27;/home/ruanyf/node_modules&#x27;</span>,</span><br><span class="line">    <span class="string">&#x27;/home/node_modules&#x27;</span>,</span><br><span class="line">    <span class="string">&#x27;/node_modules&#x27;</span> </span><br><span class="line">    <span class="string">&#x27;/home/ruanyf/.node_modules&#x27;</span>,</span><br><span class="line">    <span class="string">&#x27;/home/ruanyf/.node_libraries&#x27;</span>，</span><br><span class="line">     <span class="string">&#x27;$Prefix/lib/node&#x27;</span> ]</span><br></pre></td></tr></table></figure>
</blockquote>
<p>上面的数组，就是模块所有可能的路径。基本上是，从当前路径开始一级级向上寻找 node_modules 子目录。最后那三个路径，主要是为了历史原因保持兼容，实际上已经很少用了。</p>
<p>有了可能的路径以后，下面就是 Module._findPath() 的源码，用来确定到底哪一个是正确路径。</p>
<blockquote>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br></pre></td><td class="code"><pre><span class="line">Module._findPath = <span class="function"><span class="keyword">function</span>(<span class="params">request, paths</span>) </span>&#123;</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 列出所有可能的后缀名：.js，.json, .node</span></span><br><span class="line">  <span class="keyword">var</span> exts = <span class="built_in">Object</span>.keys(Module._extensions);</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 如果是绝对路径，就不再搜索</span></span><br><span class="line">  <span class="keyword">if</span> (request.charAt(<span class="number">0</span>) === <span class="string">&#x27;/&#x27;</span>) &#123;</span><br><span class="line">    paths = [<span class="string">&#x27;&#x27;</span>];</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 是否有后缀的目录斜杠</span></span><br><span class="line">  <span class="keyword">var</span> trailingSlash = (request.slice(-<span class="number">1</span>) === <span class="string">&#x27;/&#x27;</span>);</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 第一步：如果当前路径已在缓存中，就直接返回缓存</span></span><br><span class="line">  <span class="keyword">var</span> cacheKey = <span class="built_in">JSON</span>.stringify(&#123;<span class="attr">request</span>: request, <span class="attr">paths</span>: paths&#125;);</span><br><span class="line">  <span class="keyword">if</span> (Module._pathCache[cacheKey]) &#123;</span><br><span class="line">    <span class="keyword">return</span> Module._pathCache[cacheKey];</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 第二步：依次遍历所有路径</span></span><br><span class="line">  <span class="keyword">for</span> (<span class="keyword">var</span> i = <span class="number">0</span>, PL = paths.length; i &lt; PL; i++) &#123;</span><br><span class="line">    <span class="keyword">var</span> basePath = path.resolve(paths[i], request);</span><br><span class="line">    <span class="keyword">var</span> filename;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> (!trailingSlash) &#123;</span><br><span class="line">      <span class="comment">// 第三步：是否存在该模块文件</span></span><br><span class="line">      filename = tryFile(basePath);</span><br><span class="line"></span><br><span class="line">      <span class="keyword">if</span> (!filename &amp;&amp; !trailingSlash) &#123;</span><br><span class="line">        <span class="comment">// 第四步：该模块文件加上后缀名，是否存在</span></span><br><span class="line">        filename = tryExtensions(basePath, exts);</span><br><span class="line">      &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 第五步：目录中是否存在 package.json </span></span><br><span class="line">    <span class="keyword">if</span> (!filename) &#123;</span><br><span class="line">      filename = tryPackage(basePath, exts);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> (!filename) &#123;</span><br><span class="line">      <span class="comment">// 第六步：是否存在目录名 + index + 后缀名 </span></span><br><span class="line">      filename = tryExtensions(path.resolve(basePath, <span class="string">&#x27;index&#x27;</span>), exts);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 第七步：将找到的文件路径存入返回缓存，然后返回</span></span><br><span class="line">    <span class="keyword">if</span> (filename) &#123;</span><br><span class="line">      Module._pathCache[cacheKey] = filename;</span><br><span class="line">      <span class="keyword">return</span> filename;</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 第八步：没有找到文件，返回false </span></span><br><span class="line">  <span class="keyword">return</span> <span class="literal">false</span>;</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure>
</blockquote>
<p>经过上面代码，就可以找到模块的绝对路径了。</p>
<p>有时在项目代码中，需要调用模块的绝对路径，那么除了 module.filename ，Node 还提供一个 require.resolve 方法，供外部调用，用于从模块名取到绝对路径。</p>
<blockquote>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">require</span>.resolve = <span class="function"><span class="keyword">function</span>(<span class="params">request</span>) </span>&#123;</span><br><span class="line">  <span class="keyword">return</span> Module._resolveFilename(request, self);</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 用法</span></span><br><span class="line"><span class="built_in">require</span>.resolve(<span class="string">&#x27;a.js&#x27;</span>)</span><br><span class="line"><span class="comment">// 返回 /home/ruanyf/tmp/a.js</span></span><br></pre></td></tr></table></figure>
</blockquote>
<h2 id="11-5-加载模块"><a href="#11-5-加载模块" class="headerlink" title="11.5 加载模块"></a>11.5 加载模块</h2><p>有了模块的绝对路径，就可以加载该模块了。下面是 module.load 方法的源码。</p>
<blockquote>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">Module.prototype.load = <span class="function"><span class="keyword">function</span>(<span class="params">filename</span>) </span>&#123;</span><br><span class="line">  <span class="keyword">var</span> extension = path.extname(filename) || <span class="string">&#x27;.js&#x27;</span>;</span><br><span class="line">  <span class="keyword">if</span> (!Module._extensions[extension]) extension = <span class="string">&#x27;.js&#x27;</span>;</span><br><span class="line">  Module._extensions[extension](<span class="built_in">this</span>, filename);</span><br><span class="line">  <span class="built_in">this</span>.loaded = <span class="literal">true</span>;</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure>
</blockquote>
<p>上面代码中，首先确定模块的后缀名，不同的后缀名对应不同的加载方法。下面是 .js 和 .json 后缀名对应的处理方法。</p>
<blockquote>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line">Module._extensions[<span class="string">&#x27;.js&#x27;</span>] = <span class="function"><span class="keyword">function</span>(<span class="params"><span class="built_in">module</span>, filename</span>) </span>&#123;</span><br><span class="line">  <span class="keyword">var</span> content = fs.readFileSync(filename, <span class="string">&#x27;utf8&#x27;</span>);</span><br><span class="line">  <span class="built_in">module</span>._compile(stripBOM(content), filename);</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line">Module._extensions[<span class="string">&#x27;.json&#x27;</span>] = <span class="function"><span class="keyword">function</span>(<span class="params"><span class="built_in">module</span>, filename</span>) </span>&#123;</span><br><span class="line">  <span class="keyword">var</span> content = fs.readFileSync(filename, <span class="string">&#x27;utf8&#x27;</span>);</span><br><span class="line">  <span class="keyword">try</span> &#123;</span><br><span class="line">    <span class="built_in">module</span>.exports = <span class="built_in">JSON</span>.parse(stripBOM(content));</span><br><span class="line">  &#125; <span class="keyword">catch</span> (err) &#123;</span><br><span class="line">    err.message = filename + <span class="string">&#x27;: &#x27;</span> + err.message;</span><br><span class="line">    <span class="keyword">throw</span> err;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure>
</blockquote>
<p>这里只讨论 js 文件的加载。首先，将模块文件读取成字符串，然后剥离 utf8 编码特有的BOM文件头，最后编译该模块。</p>
<p>module._compile 方法用于模块的编译。</p>
<blockquote>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">Module.prototype._compile = <span class="function"><span class="keyword">function</span>(<span class="params">content, filename</span>) </span>&#123;</span><br><span class="line">  <span class="keyword">var</span> self = <span class="built_in">this</span>;</span><br><span class="line">  <span class="keyword">var</span> args = [self.exports, <span class="built_in">require</span>, self, filename, dirname];</span><br><span class="line">  <span class="keyword">return</span> compiledWrapper.apply(self.exports, args);</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure>
</blockquote>
<p>上面的代码基本等同于下面的形式。</p>
<blockquote>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">(<span class="function"><span class="keyword">function</span> (<span class="params"><span class="built_in">exports</span>, <span class="built_in">require</span>, <span class="built_in">module</span>, __filename, __dirname</span>) </span>&#123;</span><br><span class="line">  <span class="comment">// 模块源码</span></span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure>
</blockquote>
<p>也就是说，模块的加载实质上就是，注入exports、require、module三个全局变量，然后执行模块的源码，然后将模块的 exports 变量的值输出。</p>
<h1 id="12-Node-js-事件循环"><a href="#12-Node-js-事件循环" class="headerlink" title="12.Node.js 事件循环"></a>12.Node.js 事件循环</h1><h2 id="前言-Node-事件循环"><a href="#前言-Node-事件循环" class="headerlink" title="前言 Node 事件循环"></a>前言 Node 事件循环</h2><p>翻译完了之后，才发现有官方翻译 ; 但是本文更加全面。本文是从官方文档和多篇文章整合而来。</p>
<p>看完本文之后，你会发现这里内容与《NodeJs 深入浅出》第三章第四节 3.4 非I/O异步API 中的内容不吻合。因为书上是有些内容是错误的。<br>还有一点的是，NodeJS 的事件循环与 Javascript 的略有不同。因此需要把两者区分开。</p>
<h2 id="12-1-什么是事件循环-What-is-the-Event-Loop"><a href="#12-1-什么是事件循环-What-is-the-Event-Loop" class="headerlink" title="12.1 什么是事件循环 (What is the Event Loop)?"></a>12.1 什么是事件循环 (What is the Event Loop)?</h2><p>事件循环使 Node.js 可以通过将操作转移到系统内核中来执行非阻塞 I/O 操作（尽管 JavaScript 是单线程的）。</p>
<p>由于大多数现代内核都是多线程的，因此它们可以处理在后台执行的多个操作。 当这些操作之一完成时，内核会告诉 Node.js，以便可以将适当的回调添加到轮询队列中以最终执行。 我们将在本文的后面对此进行详细说明。</p>
<h2 id="12-2-这就是事件循环-Event-Loop-Explained"><a href="#12-2-这就是事件循环-Event-Loop-Explained" class="headerlink" title="12.2 这就是事件循环 (Event Loop Explained)"></a>12.2 这就是事件循环 (Event Loop Explained)</h2><p>Node.js 启动时，它将初始化事件循环，处理提供的输入脚本（或放入 REPL，本文档未涵盖），这些脚本可能会进行异步 API 调用，调度计时器或调用 process.nextTick， 然后开始处理事件循环。</p>
<p>下图显示了事件循环操作顺序的简化概述。</p>
<p>   ┌───────────────────────────┐<br>┌─&gt;│           timers          │<br>│  └─────────────┬─────────────┘<br>│  ┌─────────────┴─────────────┐<br>│  │     pending callbacks     │<br>│  └─────────────┬─────────────┘<br>│  ┌─────────────┴─────────────┐<br>│  │       idle, prepare       │<br>│  └─────────────┬─────────────┘      ┌───────────────┐<br>│  ┌─────────────┴─────────────┐      │   incoming:   │<br>│  │           poll            │&lt;─────┤  connections, │<br>│  └─────────────┬─────────────┘      │   data, etc.  │<br>│  ┌─────────────┴─────────────┐      └───────────────┘<br>│  │           check           │<br>│  └─────────────┬─────────────┘<br>│  ┌─────────────┴─────────────┐<br>└──┤      close callbacks      │<br>   └───────────────────────────┘<br>每个阶段都有一个要执行的回调 FIFO 队列。 尽管每个阶段都有其自己的特殊方式，但是通常，当事件循环进入给定阶段时，它将执行该阶段特定的任何操作，然后在该阶段的队列中执行回调，直到队列耗尽或执行回调的最大数量为止。 当队列已为空或达到回调限制时，事件循环将移至下一个阶段，依此类推。</p>
<p>由于这些操作中的任何一个都可能调度更多操作，并且在 poll阶段处理由内核排队的新事件 (比如 I/O 事件)，因此可以在处理 poll 事件时将 poll 事件排队。 最终导致的结果是，长时间运行的回调可使 poll 阶段运行的时间比 timer 的阈值长得多。 有关更多详细信息，请参见计时器 (timer) 和轮询 (poll) 部分。</p>
<p>注意：Windows 和 Unix / Linux 实现之间存在细微差异，但这对于本演示并不重要。 最重要的部分在这里。 实际上有七个或八个阶段，但是我们关心的那些（Node.js 实际使用的那些）是上面的阶段。</p>
<h2 id="12-3-各阶段概览-Phases-Overview"><a href="#12-3-各阶段概览-Phases-Overview" class="headerlink" title="12.3 各阶段概览 Phases Overview"></a>12.3 各阶段概览 Phases Overview</h2><ul>
<li>timers：此阶段执行由 setTimeout 和 setInterval 设置的回调。</li>
<li>pending callbacks：执行推迟到下一个循环迭代的 I/O 回调。</li>
<li>idle, prepare, ：仅在内部使用。</li>
<li>poll：取出新完成的 I/O 事件；执行与 I/O 相关的回调（除了关闭回调，计时器调度的回调和 setImmediate 之外，几乎所有这些回调） 适当时，node 将在此处阻塞。</li>
<li>check：在这里调用 setImmediate 回调。</li>
<li>close callbacks：一些关闭回调，例如 socket.on(‘close’, …)。<br>在每次事件循环运行之间，Node.js 会检查它是否正在等待任何异步 I/O 或 timers，如果没有，则将其干净地关闭。</li>
</ul>
<h2 id="12-4-各阶段详细解释-Phases-in-Detail"><a href="#12-4-各阶段详细解释-Phases-in-Detail" class="headerlink" title="12.4 各阶段详细解释 Phases in Detail"></a>12.4 各阶段详细解释 Phases in Detail</h2><h3 id="12-4-1-timers-计时器阶段"><a href="#12-4-1-timers-计时器阶段" class="headerlink" title="12.4.1 timers 计时器阶段"></a>12.4.1 timers 计时器阶段</h3><p>计时器可以在回调后面指定时间阈值，但这不是我们希望其执行的确切时间。 计时器回调将在经过指定的时间后尽早运行。 但是，操作系统调度或其他回调的运行可能会延迟它们。– 执行的实际时间不确定</p>
<p>注意：从技术上讲，轮询 (poll) 阶段控制计时器的执行时间。</p>
<p>例如，假设你计划在 100 毫秒后执行回调，然后脚本开始异步读取耗时 95 毫秒的文件：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><span class="line">const fs &#x3D; require(&#39;fs&#39;);</span><br><span class="line"></span><br><span class="line">function someAsyncOperation(callback) &#123;</span><br><span class="line">  &#x2F;&#x2F; Assume this takes 95ms to complete</span><br><span class="line">  fs.readFile(&#39;&#x2F;path&#x2F;to&#x2F;file&#39;, callback);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">const timeoutScheduled &#x3D; Date.now();</span><br><span class="line"></span><br><span class="line">setTimeout(() &#x3D;&gt; &#123;</span><br><span class="line">  const delay &#x3D; Date.now() - timeoutScheduled;</span><br><span class="line"></span><br><span class="line">  console.log(&#96;$&#123;delay&#125;ms have passed since I was scheduled&#96;);</span><br><span class="line">&#125;, 100);</span><br><span class="line"></span><br><span class="line">&#x2F;&#x2F; do someAsyncOperation which takes 95 ms to complete</span><br><span class="line">someAsyncOperation(() &#x3D;&gt; &#123;</span><br><span class="line">  const startCallback &#x3D; Date.now();</span><br><span class="line"></span><br><span class="line">  &#x2F;&#x2F; do something that will take 10ms...</span><br><span class="line">  while (Date.now() - startCallback &lt; 10) &#123;</span><br><span class="line">    &#x2F;&#x2F; do nothing</span><br><span class="line">  &#125;</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure>

<p>当事件循环进入 poll 阶段时，它有一个空队列（fs.readFile 尚未完成），因此它将等待直到达到最快的计时器 timer 阈值为止。 等待 95 ms 过去时，fs.readFile 完成读取文件，并将需要 10ms 完成的其回调添加到轮询 (poll) 队列并执行。 回调完成后，队列中不再有回调，此时事件循环已达到最早计时器 (timer) 的阈值 (100ms)，然后返回到计时器 (timer) 阶段以执行计时器的回调。 在此示例中，您将看到计划的计时器与执行的回调之间的总延迟为 105ms。</p>
<p>Note: To prevent the poll phase from starving the event loop, libuv (the C library that implements the Node.js event loop and all of the asynchronous behaviors of the platform) also has a hard maximum (system dependent) before it stops polling for more events.</p>
<p>注意：为防止轮询 poll 阶段使事件循环陷入饥饿状态 (一直等待 poll 事件)，libuv 还具有一个硬最大值限制来停止轮询。</p>
<h3 id="12-4-2-pending-callbacks-阶段"><a href="#12-4-2-pending-callbacks-阶段" class="headerlink" title="12.4.2 pending callbacks 阶段"></a>12.4.2 pending callbacks 阶段</h3><p>此阶段执行某些系统操作的回调，例如 TCP 错误。 举个例子，如果 TCP 套接字在尝试连接时收到 ECONNREFUSED，则某些 * nix 系统希望等待报告错误。 这将会在 pending callbacks 阶段排队执行。</p>
<h3 id="12-4-3-轮询-poll-阶段"><a href="#12-4-3-轮询-poll-阶段" class="headerlink" title="12.4.3 轮询 poll 阶段"></a>12.4.3 轮询 poll 阶段</h3><p>轮询阶段具有两个主要功能：</p>
<ul>
<li><p>计算应该阻塞并 I/O 轮询的时间</p>
</li>
<li><p>处理轮询队列 (poll queue) 中的事件<br>当事件循环进入轮询 (poll) 阶段并且没有任何计时器调度 (timers scheduled) 时，将发生以下两种情况之一：</p>
</li>
<li><p>如果轮询队列 (poll queue) 不为空，则事件循环将遍历其回调队列，使其同步执行，直到队列用尽或达到与系统相关的硬限制为止 (到底是哪些硬限制？)。</p>
</li>
<li><p>如果轮询队列为空，则会发生以下两种情况之一：<br>如果已通过 setImmediate 调度了脚本，则事件循环将结束轮询 poll 阶段，并继续执行 check 阶段以执行那些调度的脚本。<br>如果脚本并没有 setImmediate 设置回调，则事件循环将等待 poll 队列中的回调，然后立即执行它们。<br>一旦轮询队列 (poll queue) 为空，事件循环将检查哪些计时器 timer 已经到时间。 如果一个或多个计时器 timer 准备就绪，则事件循环将返回到计时器阶段，以执行这些计时器的回调。</p>
</li>
</ul>
<h3 id="12-4-4-检查阶段-check"><a href="#12-4-4-检查阶段-check" class="headerlink" title="12.4.4 检查阶段 check"></a>12.4.4 检查阶段 check</h3><p>此阶段允许在轮询 poll 阶段完成后立即执行回调。 如果轮询 poll 阶段处于空闲，并且脚本已使用 setImmediate 进入 check 队列，则事件循环可能会进入 check 阶段，而不是在 poll 阶段等待。</p>
<p>setImmediate 实际上是一个特殊的计时器，它在事件循环的单独阶段运行。 它使用 libuv API，该 API 计划在轮询阶段完成后执行回调。</p>
<p>通常，在执行代码时，事件循环最终将到达轮询 poll 阶段，在该阶段它将等待传入的连接，请求等。但是，如果已使用 setImmediate 设置回调并且轮询阶段变为空闲，则它将将结束并进入 check 阶段，而不是等待轮询事件。</p>
<h3 id="12-4-5-close-callbacks-阶段"><a href="#12-4-5-close-callbacks-阶段" class="headerlink" title="12.4.5 close callbacks 阶段"></a>12.4.5 close callbacks 阶段</h3><p>如果套接字或句柄突然关闭（例如 socket.destroy），则在此阶段将发出 ‘close’ 事件。 否则它将通过 process.nextTick 发出。</p>
<h2 id="12-5-setImmediate-vs-setTimeout"><a href="#12-5-setImmediate-vs-setTimeout" class="headerlink" title="12.5 setImmediate vs setTimeout"></a>12.5 setImmediate vs setTimeout</h2><p>setImmediate 和 setTimeout 相似，但是根据调用时间的不同，它们的行为也不同。</p>
<ul>
<li>setImmediate 设计为在当前轮询 poll 阶段完成后执行脚本。</li>
<li>setTimeout 计划在以毫秒为单位的最小阈值过去之后运行脚本。<br>计时器的执行顺序将根据调用它们的上下文而有所不同。 如果两者都是主模块 (main module) 中调用的，则时序将受到进程性能的限制（这可能会受到计算机上运行的其他应用程序的影响）。有点难懂，举个例子：</li>
</ul>
<p>例如，如果我们运行以下不在 I/O 回调（即主模块）内的脚本，则两个计时器的执行顺序是不确定的，因为它受进程性能的约束：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line">&#x2F;&#x2F; timeout_vs_immediate.js</span><br><span class="line">setTimeout(() &#x3D;&gt; &#123;</span><br><span class="line">  console.log(&#39;timeout&#39;);</span><br><span class="line">&#125;, 0);</span><br><span class="line"></span><br><span class="line">setImmediate(() &#x3D;&gt; &#123;</span><br><span class="line">  console.log(&#39;immediate&#39;);</span><br><span class="line">&#125;);</span><br><span class="line">$ node timeout_vs_immediate.js</span><br><span class="line">timeout</span><br><span class="line">immediate</span><br><span class="line"></span><br><span class="line">$ node timeout_vs_immediate.js</span><br><span class="line">immediate</span><br><span class="line">timeout</span><br></pre></td></tr></table></figure>


<p>但是，如果这两个调用在一个 I/O 回调中，那么 immediate 总是执行第一：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">&#x2F;&#x2F; timeout_vs_immediate.js</span><br><span class="line">const fs &#x3D; require(&#39;fs&#39;);</span><br><span class="line"></span><br><span class="line">fs.readFile(__filename, () &#x3D;&gt; &#123;</span><br><span class="line">  setTimeout(() &#x3D;&gt; &#123;</span><br><span class="line">    console.log(&#39;timeout&#39;);</span><br><span class="line">  &#125;, 0);</span><br><span class="line">  setImmediate(() &#x3D;&gt; &#123;</span><br><span class="line">    console.log(&#39;immediate&#39;);</span><br><span class="line">  &#125;);</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure>


<p>与 setTimeout 相比，使用 setImmediate 的主要优点是，如果在 I/O 周期内 setImmediate 总是比任何 timers 快。这个可以在下方彩色图中找到答案：poll 阶段用 setImmediate 设置下阶段 check 的回调，等到了 check 就开始执行；timers 阶段只能等到下次循环执行！</p>
<p>问题：那为什么在外部 (比如主代码部分 mainline) 这两者的执行顺序不确定呢？</p>
<p>解答：在 mainline 部分执行 setTimeout 设置定时器 (没有写入队列呦)，与 setImmediate 写入 check 队列。mainline 执行完开始事件循环，第一阶段是 timers，这时候 timers 队列可能为空，也可能有回调；如果没有那么执行 check 队列的回调，下一轮循环在检查并执行 timers 队列的回调；如果有就先执行 timers 的回调，再执行 check 阶段的回调。因此这是 timers 的不确定性导致的。</p>
<p>举一反三：timers 阶段写入 check 队列</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">setTimeout(() &#x3D;&gt; &#123;</span><br><span class="line">    setTimeout(() &#x3D;&gt; &#123;</span><br><span class="line">        console.log(&#39;timeout&#39;);</span><br><span class="line">    &#125;, 0);</span><br><span class="line">    setImmediate(() &#x3D;&gt; &#123;</span><br><span class="line">        console.log(&#39;immediate&#39;);</span><br><span class="line">    &#125;);</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure>


<p>总是会输出：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">immediate</span><br><span class="line">timeout</span><br></pre></td></tr></table></figure>

<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br></pre></td><td class="code"><pre><span class="line">const ITERATIONS_MAX &#x3D; 2;</span><br><span class="line">let iteration &#x3D; 0;</span><br><span class="line"></span><br><span class="line">const timeout &#x3D; setInterval(() &#x3D;&gt; &#123;</span><br><span class="line">    console.log(&#39;TIME PHASE START:&#39; + iteration);</span><br><span class="line">    if (iteration &gt;&#x3D; ITERATIONS_MAX) &#123;</span><br><span class="line">        clearInterval(timeout);</span><br><span class="line">        console.log(&#39;TIME PHASE exceeded!&#39;);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    console.log(&#39;TIME PHASE END:&#39; + iteration);</span><br><span class="line"></span><br><span class="line">    ++iteration;</span><br><span class="line">&#125;, 0);</span><br><span class="line"></span><br><span class="line">setTimeout(() &#x3D;&gt; &#123;</span><br><span class="line">    console.log(&#39;TIME PHASE0&#39;);</span><br><span class="line">        setTimeout(() &#x3D;&gt; &#123;</span><br><span class="line">        console.log(&#39;TIME PHASE1&#39;);</span><br><span class="line"></span><br><span class="line">        setTimeout(() &#x3D;&gt; &#123;</span><br><span class="line">            console.log(&#39;TIME PHASE2&#39;);</span><br><span class="line">        &#125;);</span><br><span class="line">    &#125;);</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure>


<p>输出：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line">TIME PHASE START:0</span><br><span class="line">TIME PHASE END:0</span><br><span class="line">TIME PHASE0</span><br><span class="line">TIME PHASE START:1</span><br><span class="line">TIME PHASE END:1</span><br><span class="line">TIME PHASE1</span><br><span class="line">TIME PHASE START:2</span><br><span class="line">TIME PHASE exceeded!</span><br><span class="line">TIME PHASE END:2</span><br><span class="line">TIME PHASE2</span><br></pre></td></tr></table></figure>


<p>这表明，可以理解 setInterval 是 setTimeout 的嵌套调用的语法糖。setInterval(() =&gt; {}, 0) 是在每一次事件循环中添加回调到 timers 队列。因此不会阻止事件循环的继续运行，在浏览器上也不会感到卡顿。</p>
<h2 id="12-6process-nextTick"><a href="#12-6process-nextTick" class="headerlink" title="12.6process.nextTick"></a>12.6process.nextTick</h2><h3 id="12-6-1-理解-process-nextTick"><a href="#12-6-1-理解-process-nextTick" class="headerlink" title="12.6.1 理解 process.nextTick"></a>12.6.1 理解 process.nextTick</h3><p>你可能已经注意到 process.nextTick 并未显示在图中，即使它是异步 API 的一部分也是如此。 这是因为 process.nextTick 从技术上讲不是事件循环的一部分。 相反，无论事件循环的当前阶段如何，都将在当前操作完成之后处理 nextTickQueue。 在此，将操作定义为在 C/C ++ 处理程序基础下过渡并处理需要执行的 JavaScript。</p>
<p>回顾一下我们的图，在给定阶段里可以在任意时间调用 process.nextTick，传递给 process.nextTick 的所有回调都将在事件循环继续之前得到解决。 这可能会导致一些不良情况，因为它允许您通过进行递归 process.nextTick 调用来让 I/O 处于 “饥饿” 状态，从而防止事件循环进入轮询 poll 阶段。</p>
<p>注意：Microtask callbacks 微服务</p>
<h3 id="12-6-2-为什么允许这样操作？-Why-would-that-be-allowed"><a href="#12-6-2-为什么允许这样操作？-Why-would-that-be-allowed" class="headerlink" title="12.6.2 为什么允许这样操作？ Why would that be allowed?"></a>12.6.2 为什么允许这样操作？ Why would that be allowed?</h3><p>为什么这样的东西会包含在 Node.js 中？ 它的一部分是一种设计理念，即使不是必须的情况下，API 也应始终是异步的。</p>
<p>举个例子：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">function apiCall(arg, callback) &#123;</span><br><span class="line">  if (typeof arg !&#x3D;&#x3D; &#39;string&#39;)</span><br><span class="line">    return process.nextTick(callback,</span><br><span class="line">                            new TypeError(&#39;argument should be string&#39;));</span><br><span class="line">&#125;</span><br><span class="line">apiCall(1, e &#x3D;&gt; console.log(e));</span><br><span class="line">console.log(2);</span><br><span class="line">&#x2F;&#x2F; 2</span><br><span class="line">&#x2F;&#x2F; 1</span><br></pre></td></tr></table></figure>


<p>该代码段会进行参数检查，如果不正确，则会将错误传递给回调。 该 API 最近进行了更新，以允许将参数传递给 process.nextTick，从而可以将回调后传递的所有参数都传播为回调的参数，因此您不必嵌套函数。</p>
<p>我们正在做的是将错误传递回用户，但只有在我们允许其余用户的代码执行之后。 通过使用 process.nextTick，我们保证 apiCall 始终在用户的其余代码之后以及事件循环继续下阶段之前运行其回调。 为此，允许 JS 调用堆栈展开，然后立即执行所提供的回调，该回调可以对 process.nextTick 进行递归调用，而不会达到 RangeError：v8 超出最大调用堆栈大小。</p>
<p>这种理念可能会导致某些潜在的问题情况。 以下代码段为例：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line">let bar;</span><br><span class="line"></span><br><span class="line">&#x2F;&#x2F; this has an asynchronous signature, but calls callback synchronously</span><br><span class="line">function someAsyncApiCall(callback) &#123; callback(); &#125;</span><br><span class="line"></span><br><span class="line">&#x2F;&#x2F; the callback is called before &#96;someAsyncApiCall&#96; completes.</span><br><span class="line">someAsyncApiCall(() &#x3D;&gt; &#123;</span><br><span class="line">  &#x2F;&#x2F; since someAsyncApiCall has completed, bar hasn&#39;t been assigned any value</span><br><span class="line">  console.log(&#39;bar&#39;, bar); &#x2F;&#x2F; undefined</span><br><span class="line">&#125;);</span><br><span class="line"></span><br><span class="line">bar &#x3D; 1;</span><br></pre></td></tr></table></figure>


<p>用户将 someAsyncApiCall 定义为具有异步签名，但实际上它是同步运行的。 调用它时，提供给 someAsyncApiCall 的回调在事件循环的同一阶段被调用，因为 someAsyncApiCall 实际上并不异步执行任何操作。 结果，即使脚本可能尚未在范围内，该回调也会尝试引用 bar，因为该脚本无法运行完毕。</p>
<p>通过将回调放置在 process.nextTick 中，脚本仍具有运行完成的能力，允许在调用回调之前初始化所有变量，函数等。 它还具有不允许事件循环继续下个阶段的优点。 在允许事件循环继续之前，向用户发出错误提示可能很有用。 这是使用 process.nextTick 的先前示例：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">let bar;</span><br><span class="line"></span><br><span class="line">function someAsyncApiCall(callback) &#123;</span><br><span class="line">  process.nextTick(callback);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">someAsyncApiCall(() &#x3D;&gt; &#123;</span><br><span class="line">  console.log(&#39;bar&#39;, bar); &#x2F;&#x2F; 1</span><br><span class="line">&#125;);</span><br><span class="line"></span><br><span class="line">bar &#x3D; 1;</span><br></pre></td></tr></table></figure>


<p>这是另一个真实的例子：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">const server &#x3D; net.createServer(() &#x3D;&gt; &#123;&#125;).listen(8080);</span><br><span class="line"></span><br><span class="line">server.on(&#39;listening&#39;, () &#x3D;&gt; &#123;&#125;);</span><br></pre></td></tr></table></figure>


<p>仅通过端口时，该端口将立即绑定。 因此，可以立即调用 “监听” 回调。 问题在于那时尚未设置.on(‘listening’) 回调。</p>
<p>为了解决这个问题，”listening” 事件在 nextTick() 中排队，以允许脚本运行完成。 这允许用户设置他们想要的任何事件处理程序。</p>
<h3 id="12-6-3-process-nextTick-vs-setImmediate"><a href="#12-6-3-process-nextTick-vs-setImmediate" class="headerlink" title="12.6.3 process.nextTick vs setImmediate"></a>12.6.3 process.nextTick vs setImmediate</h3><p>他们的调用方式很相似，但是名称让人困惑。</p>
<ul>
<li>process.nextTick 在同一阶段立即触发</li>
<li>setImmediate fires on the following iteration or ‘tick’ of the event loop (在事件循环接下来的阶段迭代中执行 - check 阶段)。</li>
</ul>
<p>本质上，名称应互换。 process.nextTick 比 setImmediate 触发得更快，但由于历史原因，不太可能改变。 进行此切换将破坏 npm 上很大一部分软件包。 每天都会添加更多的新模块，这意味着我们每天都在等待，更多潜在的损坏发生。 尽管它们令人困惑，但名称本身不会改变。</p>
<p>我们建议开发人员在所有情况下都使用 setImmediate，因为这样更容易推理（并且代码与各种环境兼容，例如浏览器 JS。）- 但是如果理解底层原理，就不一样。</p>
<h3 id="12-6-4-为什么还用-process-nextTick？"><a href="#12-6-4-为什么还用-process-nextTick？" class="headerlink" title="12.6.4 为什么还用 process.nextTick？"></a>12.6.4 为什么还用 process.nextTick？</h3><ul>
<li>这里举出两个原因：</li>
<li>在事件循环继续之前下个阶段允许开发者处理错误，清理所有不必要的资源，或者重新尝试请求。<br>有时需要让回调在事件循环继续下个阶段之前运行 (At times it’s necessary to allow a callback to run after the call stack has unwound but before the event loop continues.)。</li>
</ul>
<p>简单的例子：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">const server &#x3D; net.createServer();</span><br><span class="line">server.on(&#39;connection&#39;, (conn) &#x3D;&gt; &#123; &#125;);</span><br><span class="line"></span><br><span class="line">server.listen(8080);</span><br><span class="line">server.on(&#39;listening&#39;, () &#x3D;&gt; &#123; &#125;); &#x2F;&#x2F; 设置监听回调</span><br></pre></td></tr></table></figure>

<p>假设 listen 在事件循环的开始处运行，但是侦听回调被放置在 setImmediate 中 (实际上 listen 使用 process.nextTick,.on 在本阶段完成)。 除非传递主机名，否则将立即绑定到端口。 为了使事件循环继续进行，它必须进入轮询 poll 阶段，这意味着存在已经接收到连接可能性，从而导致在侦听事件之前触发连接事件 (漏掉一些 poll 事件)。</p>
<p>另一个示例正在运行一个要从 EventEmitter 继承的函数构造函数，它想在构造函数中调用一个事件：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line">const EventEmitter &#x3D; require(&#39;events&#39;);</span><br><span class="line">const util &#x3D; require(&#39;util&#39;);</span><br><span class="line"></span><br><span class="line">function MyEmitter() &#123;</span><br><span class="line">  EventEmitter.call(this);</span><br><span class="line">  this.emit(&#39;event&#39;);</span><br><span class="line">&#125;</span><br><span class="line">util.inherits(MyEmitter, EventEmitter);</span><br><span class="line"></span><br><span class="line">const myEmitter &#x3D; new MyEmitter();</span><br><span class="line">myEmitter.on(&#39;event&#39;, () &#x3D;&gt; &#123;</span><br><span class="line">  console.log(&#39;an event occurred!&#39;);</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure>


<p>你无法立即从构造函数中发出事件，因为脚本还没运行到开发者为该事件分配回调的那里 (指 myEmitter.on)。 因此，在构造函数本身内，你可以使用 process.nextTick 设置构造函数完成后发出事件的回调，从而提供预期的结果：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line">const EventEmitter &#x3D; require(&#39;events&#39;);</span><br><span class="line">const util &#x3D; require(&#39;util&#39;);</span><br><span class="line"></span><br><span class="line">function MyEmitter() &#123;</span><br><span class="line">  EventEmitter.call(this);</span><br><span class="line"></span><br><span class="line">  &#x2F;&#x2F; use nextTick to emit the event once a handler is assigned</span><br><span class="line">  process.nextTick(() &#x3D;&gt; &#123;</span><br><span class="line">    this.emit(&#39;event&#39;);</span><br><span class="line">  &#125;);</span><br><span class="line">&#125;</span><br><span class="line">util.inherits(MyEmitter, EventEmitter);</span><br><span class="line"></span><br><span class="line">const myEmitter &#x3D; new MyEmitter();</span><br><span class="line">myEmitter.on(&#39;event&#39;, () &#x3D;&gt; &#123;</span><br><span class="line">  console.log(&#39;an event occurred!&#39;);</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure>

<h3 id="12-6-5-process-nextTick-在事件循环的位置："><a href="#12-6-5-process-nextTick-在事件循环的位置：" class="headerlink" title="12.6.5 process.nextTick 在事件循环的位置："></a>12.6.5 process.nextTick 在事件循环的位置：</h3><p>来子一位外国小哥之手。链接在本文下面。</p>
<pre><code>       ┌───────────────────────────┐
    ┌─&gt;│           timers          │
    │  └─────────────┬─────────────┘
    │           nextTickQueue
    │  ┌─────────────┴─────────────┐
    │  │     pending callbacks     │
    │  └─────────────┬─────────────┘
    │           nextTickQueue
    │  ┌─────────────┴─────────────┐
    |  |     idle, prepare         │
    |  └─────────────┬─────────────┘
</code></pre>
<p>  nextTickQueue     nextTickQueue<br>        |  ┌─────────────┴─────────────┐<br>        |  │           poll            │<br>        │  └─────────────┬─────────────┘<br>        │           nextTickQueue<br>        │  ┌─────────────┴─────────────┐<br>        │  │           check           │<br>        │  └─────────────┬─────────────┘<br>        │           nextTickQueue<br>        │  ┌─────────────┴─────────────┐<br>        └──┤       close callbacks     │<br>           └───────────────────────────┘<br>下图补充了官方并没有提及的 Microtasks 微任务:</p>
<p><img data-src="https://cdn.learnku.com/uploads/images/201912/27/20604/NclgxSvw3g.png!large" alt="img"></p>
<h2 id="12-7-Microtasks-微任务"><a href="#12-7-Microtasks-微任务" class="headerlink" title="12.7 Microtasks 微任务"></a>12.7 Microtasks 微任务</h2><p>微任务会在主线之后和事件循环的每个阶段之后立即执行。</p>
<p>如果您熟悉 JavaScript 事件循环，那么应该对微任务不陌生，这些微任务在 Node 中的工作方式相同。 如果你想重新了解事件循环和微任务队列，请查看此链接（这东西非常底层，慎点）。</p>
<p>在 Node 领域，微任务是来自以下对象的回调：</p>
<ul>
<li>process.nextTick()</li>
<li>then() handlers for resolved or rejected Promises</li>
</ul>
<p>在主线结束后以及事件循环的每个阶段之后，立即运行微任务回调。</p>
<p>resolved 的 promise.then 回调像微处理一样执行，就像 process.nextTick 一样。 虽然，如果两者都在同一个微任务队列中，则将首先执行 process.nextTick 的回调。</p>
<p>优先级 process.nextTick &gt; promise.then = queueMicrotask</p>
<p>下面例子完整演示了事件循环：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br></pre></td><td class="code"><pre><span class="line">const fs &#x3D; require(&#39;fs&#39;);</span><br><span class="line">const logger &#x3D; require(&#39;..&#x2F;common&#x2F;logger&#39;);</span><br><span class="line">const ITERATIONS_MAX &#x3D; 2;</span><br><span class="line">let iteration &#x3D; 0;</span><br><span class="line">const start &#x3D; Date.now();</span><br><span class="line">const msleep &#x3D; (i) &#x3D;&gt; &#123;</span><br><span class="line">    for (let index &#x3D; 0; Date.now() - start &lt; i; index++) &#123;</span><br><span class="line">        &#x2F;&#x2F; do nonthing</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line">Promise.resolve().then(() &#x3D;&gt; &#123;</span><br><span class="line">    &#x2F;&#x2F; Microtask callback runs AFTER mainline, even though the code is here</span><br><span class="line">    logger.info(&#39;Promise.resolve.then&#39;, &#39;MAINLINE MICROTASK&#39;);</span><br><span class="line">&#125;);</span><br><span class="line">logger.info(&#39;START&#39;, &#39;MAINLINE&#39;);</span><br><span class="line">const timeout &#x3D; setInterval(() &#x3D;&gt; &#123;</span><br><span class="line">    logger.info(&#39;START iteration &#39; + iteration + &#39;: setInterval&#39;, &#39;TIMERS PHASE&#39;);</span><br><span class="line">    if (iteration &lt; ITERATIONS_MAX) &#123;</span><br><span class="line">        setTimeout((iteration) &#x3D;&gt; &#123;</span><br><span class="line">            logger.info(&#39;TIMER EXPIRED (from iteration &#39; + iteration + &#39;): setInterval.setTimeout&#39;, &#39;TIMERS PHASE&#39;);</span><br><span class="line">            Promise.resolve().then(() &#x3D;&gt; &#123;</span><br><span class="line">                logger.info(&#39;setInterval.setTimeout.Promise.resolve.then&#39;, &#39;TIMERS PHASE MICROTASK&#39;);</span><br><span class="line">            &#125;);</span><br><span class="line">        &#125;, 0, iteration);</span><br><span class="line">        fs.readdir(__dirname, (err, files) &#x3D;&gt; &#123;</span><br><span class="line">            if (err) throw err;</span><br><span class="line">            logger.info(&#39;fs.readdir() callback: Directory contains: &#39; + files.length + &#39; files&#39;, &#39;POLL PHASE&#39;);</span><br><span class="line">            queueMicrotask(() &#x3D;&gt; logger.info(&#39;setInterval.fs.readdir.queueMicrotask&#39;, &#39;POLL PHASE MICROTASK&#39;));</span><br><span class="line">            Promise.resolve().then(() &#x3D;&gt; &#123;</span><br><span class="line">                logger.info(&#39;setInterval.fs.readdir.Promise.resolve.then&#39;, &#39;POLL PHASE MICROTASK&#39;);</span><br><span class="line">            &#125;);</span><br><span class="line">        &#125;);</span><br><span class="line">        setImmediate(() &#x3D;&gt; &#123;</span><br><span class="line">            logger.info(&#39;setInterval.setImmediate&#39;, &#39;CHECK PHASE&#39;);</span><br><span class="line">            Promise.resolve().then(() &#x3D;&gt; &#123;</span><br><span class="line">                logger.info(&#39;setInterval.setTimeout.Promise.resolve.then&#39;, &#39;CHECK PHASE MICROTASK&#39;);</span><br><span class="line">            &#125;);</span><br><span class="line">        &#125;);</span><br><span class="line">        &#x2F;&#x2F; msleep(1000); &#x2F;&#x2F; 等待 I&#x2F;O 完成</span><br><span class="line">    &#125; else &#123;</span><br><span class="line">        logger.info(&#39;Max interval count exceeded. Goodbye.&#39;, &#39;TIMERS PHASE&#39;);</span><br><span class="line">        clearInterval(timeout);</span><br><span class="line">    &#125;</span><br><span class="line">    logger.info(&#39;END iteration &#39; + iteration + &#39;: setInterval&#39;, &#39;TIMERS PHASE&#39;);</span><br><span class="line">    iteration++;</span><br><span class="line">&#125;, 0);</span><br><span class="line">logger.info(&#39;END&#39;, &#39;MAINLINE&#39;);</span><br></pre></td></tr></table></figure>


<p>输出：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br></pre></td><td class="code"><pre><span class="line">1577168519233:INFO: MAINLINE: START</span><br><span class="line">1577168519242:INFO: MAINLINE: END</span><br><span class="line">1577168519243:INFO: MAINLINE MICROTASK: Promise.resolve.then</span><br><span class="line"></span><br><span class="line"># 第一次</span><br><span class="line"></span><br><span class="line">1577168519243:INFO: TIMERS PHASE: START iteration 0: setInterval</span><br><span class="line">1577168519244:INFO: TIMERS PHASE: END iteration 0: setInterval</span><br><span class="line"></span><br><span class="line">## 到这里循环已经结束了</span><br><span class="line"></span><br><span class="line">## 这时候 timers 阶段为空, poll 阶段有新事件完成</span><br><span class="line"></span><br><span class="line">1577168519245:INFO: POLL PHASE: fs.readdir() callback: Directory contains: 2 files</span><br><span class="line">1577168519245:INFO: POLL PHASE MICROTASK: setInterval.fs.readdir.queueMicrotask</span><br><span class="line">1577168519245:INFO: POLL PHASE MICROTASK: setInterval.fs.readdir.Promise.resolve.then</span><br><span class="line"></span><br><span class="line">## 在 poll 阶段结束后马上处理微任务</span><br><span class="line"></span><br><span class="line">## poll 转 check 阶段执行 setImmediate 设置的回调</span><br><span class="line"></span><br><span class="line">1577168519245:INFO: CHECK PHASE: setInterval.setImmediate</span><br><span class="line">1577168519245:INFO: CHECK PHASE MICROTASK: setInterval.setTimeout.Promise.resolve.then</span><br><span class="line"></span><br><span class="line">## 开始新的循环, timers 队列不为空</span><br><span class="line"></span><br><span class="line">1577168519246:INFO: TIMERS PHASE: TIMER EXPIRED (from iteration 0): setInterval.setTimeout</span><br><span class="line">1577168519246:INFO: TIMERS PHASE MICROTASK: setInterval.setTimeout.Promise.resolve.then</span><br><span class="line"></span><br><span class="line"># 第二次</span><br><span class="line"></span><br><span class="line">1577168519246:INFO: TIMERS PHASE: START iteration 1: setInterval</span><br><span class="line">1577168519246:INFO: TIMERS PHASE: END iteration 1: setInterval</span><br><span class="line"></span><br><span class="line">1577168519246:INFO: CHECK PHASE: setInterval.setImmediate</span><br><span class="line">1577168519246:INFO: CHECK PHASE MICROTASK: setInterval.setTimeout.Promise.resolve.then</span><br><span class="line"></span><br><span class="line">1577168519246:INFO: POLL PHASE: fs.readdir() callback: Directory contains: 2 files</span><br><span class="line">1577168519253:INFO: POLL PHASE MICROTASK: setInterval.fs.readdir.queueMicrotask</span><br><span class="line">1577168519253:INFO: POLL PHASE MICROTASK: setInterval.fs.readdir.Promise.resolve.then</span><br><span class="line"></span><br><span class="line">1577168519253:INFO: TIMERS PHASE: TIMER EXPIRED (from iteration 1): setInterval.setTimeout</span><br><span class="line">1577168519253:INFO: TIMERS PHASE MICROTASK: setInterval.setTimeout.Promise.resolve.then</span><br><span class="line"></span><br><span class="line"># 第三次退出</span><br><span class="line"></span><br><span class="line">1577168519253:INFO: TIMERS PHASE: START iteration 2: setInterval</span><br><span class="line">1577168519253:INFO: TIMERS PHASE: Max interval count exceeded. Goodbye.</span><br><span class="line">1577168519253:INFO: TIMERS PHASE: END iteration 2: setInterval</span><br></pre></td></tr></table></figure>


<p>运行结果的顺序不固定，因为 fs.readdir 需要 I/O 系统调用，需要等待系统的调度，因此等待事件并不固定。</p>
<p>但是顺序仍然是有规律的：</p>
<ul>
<li>因为 setTimeout 和 setImmediate 在 timers 阶段 (不是 mainline 就行) 被调用，因此 setImmediate 总是比 setTimeout 快 (前面第 5 节已说明)</li>
<li>因为 poll 阶段等待系统调用的时间不确定。因此它会在上面两者之间插空，就是 3 种排序<br>poll check timers 这种可能比较少，取决于 I/O 调用速度与进程在当前 timers 阶段的处理时间 —— 也就是 I/O 的事件循环进入 poll 阶段前就已经完成，也就是 poll 队列不为空。把上面的 msleep 注释打开即可测试。<br>check poll timers 这种情况比较多出现。<br>check timers poll 这种情况也多。</li>
</ul>
<p>因此存在 3 种顺序。</p>
<p>本文下方链接包含更多例子</p>
<p>timers 阶段和 poll 阶段，因为依赖系统的调度，所以具体在哪一次事件循环执行？这是不确定的，有可能是下次循环就可以，也许需要等待。在上面彩色图的事件循环中黄色标记的阶段中，只剩下 check 阶段是确定的 —— 必然是在本次 (还没到本次循环的 check 阶段的话) 或者下次循环调用。还有的是，微服务是能够保证，必然在本阶段结束后下阶段前执行。</p>
<p>timers 不确定，poll 不确定，check 确定，Microtasks 确定。</p>
<h2 id="12-8题外话：Events"><a href="#12-8题外话：Events" class="headerlink" title="12.8题外话：Events"></a>12.8题外话：Events</h2><p>事件是应用程序中发生的重要事件。 诸如 Node 之类的事件驱动的运行时在某些地方发出事件，并在其他地方响应事件。</p>
<p>例子：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line">&#x2F;&#x2F; The Node EventEmitter</span><br><span class="line">const EventEmitter &#x3D; require(&#39;events&#39;);</span><br><span class="line">&#x2F;&#x2F; Create an instance of EventEmitter</span><br><span class="line">const eventEmitter &#x3D; new EventEmitter();</span><br><span class="line"></span><br><span class="line">&#x2F;&#x2F; The common logger</span><br><span class="line">const logger &#x3D; require(&#39;..&#x2F;common&#x2F;logger&#39;);</span><br><span class="line"></span><br><span class="line">logger.info(&#39;START&#39;, &#39;MAINLINE&#39;);</span><br><span class="line"></span><br><span class="line">logger.info(&#39;Registering simpleEvent handler&#39;, &#39;MAINLINE&#39;);</span><br><span class="line">eventEmitter.on(&#39;simpleEvent&#39;, (eventName, message, source, timestamp) &#x3D;&gt; &#123;</span><br><span class="line">logger.info(&#39;Received event: &#39; + timestamp + &#39;: &#39; + source + &#39;:[&#39; + eventName + &#39;]: &#39; + message, &#39;EventEmitter.on()&#39;);</span><br><span class="line">&#125;);</span><br><span class="line"></span><br><span class="line">&#x2F;&#x2F; Get the current time</span><br><span class="line">let hrtime &#x3D; process.hrtime();</span><br><span class="line">eventEmitter.emit(&#39;simpleEvent&#39;, &#39;simpleEvent&#39;, &#39;Custom event says what?&#39;, &#39;MAINLINE&#39;, (hrtime[0] * 1e9 + hrtime[1] ) &#x2F; 1e6);</span><br><span class="line"></span><br><span class="line">logger.info(&#39;END&#39;, &#39;MAINLINE&#39;);</span><br></pre></td></tr></table></figure>


<p>输出：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">$ node example7</span><br><span class="line">1530379926998:INFO: MAINLINE: START</span><br><span class="line">1530379927000:INFO: MAINLINE: Registering simpleEvent handler</span><br><span class="line">1530379927000:INFO: EventEmitter.on(): Received event: 553491474.966337: MAINLINE:[simpleEvent]: Custom event says what?</span><br><span class="line">1530379927000:INFO: MAINLINE: END</span><br></pre></td></tr></table></figure>


<p>上面结果看出，Event 是同步，什么时候 emit 就什么时候执行回调。</p>
<h1 id="13-cluster原理"><a href="#13-cluster原理" class="headerlink" title="13.cluster原理"></a>13.<a target="_blank" rel="noopener" href="https://www.cnblogs.com/dashnowords/p/10958457.html">cluster原理</a></h1><h2 id="13-1-概述"><a href="#13-1-概述" class="headerlink" title="13.1 概述"></a>13.1 概述</h2><p><code>cluster</code>模块是<code>node.js</code>中用于实现和管理多进程的模块。常规的<code>node.js</code>应用程序是单线程单进程的，这也意味着它很难充分利用服务器多核CPU的性能，而<code>cluster</code>模块就是为了解决这个 问题的，它使得<code>node.js</code>程序可以以多个实例并存的方式运行在不同的进程中，以求更大地榨取服务器的性能。<code>node.js</code>在官方示例代码中使用<code>worker</code>实例来表示主进程fork出的子进程，使得前端开发者在学习过程中非常容易和浏览器环境中的<code>worker</code>实现的多线程混淆。为了容易区分，我们和<code>node</code>官方文档使用一致的名称，用集群中的<code>master</code>和<code>worker</code>来区分主进程和工作进程，用<code>worker_threads</code>来描述工作线程。</p>
<p><code>node.js</code>的主从模型中，<code>master</code>主进程相当于一个包工头，主管监听端口，而<code>slave</code>进程被用于实际的任务执行，当任务请求到达后，它会根据某种方式将连接循环分发给<code>worker</code>进程来处理。理论上，如果根据当前各个<code>worker</code>进程的负载状况或者相关信息来挑选工作进程，效率应该比直接循环发放要更高，但<code>node.js</code>文档中声明这种方式由于受到操作系统调度机制的影响，会使得分发变得不稳定，所以会将**”循环法”**作为默认的分发策略。</p>
<p>关于<code>cluster</code>模块的用法和<strong>API</strong>细节，可以直接参考官方文档<a target="_blank" rel="noopener" href="http://nodejs.cn/api/cluster.html">《Node.js中文网V10.15.3/cluster》</a>。</p>
<h2 id="13-2-线程与进程"><a href="#13-2-线程与进程" class="headerlink" title="13.2 线程与进程"></a>13.2 线程与进程</h2><p>想要尽可能利用服务器性能，首先需要了解“线程”（thread）和“进程”（process）这两个概念。</p>
<p>计算机是由CPU来执行计算任务的，如果你只有一个CPU，那么这台机器上所有的任务都将由它来执行。它既可以按照串联执行的原则一个接一个执行任务，也可以依据并联原则同步执行多个任务，多个任务同步执行时，CPU会快速在多个线程之间进行切换，切换线程的同时要切换对应任务的上下文，这就会造成额外的CPU资源消耗，所以当线程数量非常多时，线程切换本身就会浪费大量的CPU资源。如果在执行一个任务的同时，CPU和内存都还有充足的剩余，就可以通过某种方式让它们去执行其他任务。</p>
<blockquote>
<p>你可以将“线程”看作是一种轻量级的“进程”。</p>
</blockquote>
<p>如果你在操作系统中打开任务管理器，在<code>进程</code>标签下就可以看到如下图的示例：</p>
<p><img data-src="https://img2018.cnblogs.com/blog/1354575/201906/1354575-20190601090434633-1515670765.png" alt="img"></p>
<p>我们可以看到每一个程序<strong>至少</strong>开辟一个新的进程（你可能瞬间就明白了chrome效率高的原因，我什么都没说），它是一种粒度更大的资源隔离单元，进程之间使用不同的内存区域，无法直接共享数据，只能通过跨进程通讯机制来通讯，而且由于要使用新的内存区域，它的创建销毁和切换相对而言都更耗时，它的好处就是进程之间是互相隔离的，互不影响，所以你可以一边听音乐一边玩游戏，而不会因为音乐软件里突然放了一首轻音乐，结果你游戏里的角色攻击力减半了。</p>
<p>再来看一下<code>性能</code>这个标签：</p>
<p><img data-src="https://img2018.cnblogs.com/blog/1354575/201906/1354575-20190601090447906-116651620.png" alt="img"></p>
<p>可以看到线程数是远大于进程数的。“线程”通常用来在单个“进程”中提高CPU的利用率，它是一种粒度更细的资源调度单位，它更容易创建和销毁，在同一个进程内的线程共享分配给这个进程的内存，所以也就实现了共享数据，多线程的编程要更加复杂，由于共享数据，如果线程之间传递指针然后操作同一数据源，就必须考虑“原子操作”和“锁”的问题，否则很容易就乱套了，如果传递数据的拷贝，又会造成内存浪费，另外线程异常不会被隔离，而会导致整个进程异常。</p>
<p>线程和进程的相关知识涉及到底层操作系统的内容，笔者涉猎有限，先分享这么多（会的都告诉你了，还要我怎样）。</p>
<h2 id="13-3-cluster模块源码解析"><a href="#13-3-cluster模块源码解析" class="headerlink" title="13.3 cluster模块源码解析"></a>13.3 cluster模块源码解析</h2><h3 id="13-3-1-起步"><a href="#13-3-1-起步" class="headerlink" title="13.3.1 起步"></a>13.3.1 起步</h3><p><code>cluster</code>模块的用法看起来并不复杂，官方给出的示例是这样的：</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> cluster = <span class="built_in">require</span>(<span class="string">&#x27;cluster&#x27;</span>);</span><br><span class="line"><span class="keyword">const</span> http = <span class="built_in">require</span>(<span class="string">&#x27;http&#x27;</span>);</span><br><span class="line"><span class="keyword">const</span> numCPUs = <span class="built_in">require</span>(<span class="string">&#x27;os&#x27;</span>).cpus().length;</span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> (cluster.isMaster) &#123;</span><br><span class="line">  <span class="built_in">console</span>.log(<span class="string">`主进程 <span class="subst">$&#123;process.pid&#125;</span> 正在运行`</span>);</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 衍生工作进程。</span></span><br><span class="line">  <span class="keyword">for</span> (<span class="keyword">let</span> i = <span class="number">0</span>; i &lt; numCPUs; i++) &#123;</span><br><span class="line">    cluster.fork();</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  cluster.on(<span class="string">&#x27;exit&#x27;</span>, <span class="function">(<span class="params">worker, code, signal</span>) =&gt;</span> &#123;</span><br><span class="line">    <span class="built_in">console</span>.log(<span class="string">`工作进程 <span class="subst">$&#123;worker.process.pid&#125;</span> 已退出`</span>);</span><br><span class="line">  &#125;);</span><br><span class="line">&#125; <span class="keyword">else</span> &#123;</span><br><span class="line">  <span class="comment">// 工作进程可以共享任何 TCP 连接。</span></span><br><span class="line">  <span class="comment">// 在本例子中，共享的是 HTTP 服务器。</span></span><br><span class="line">  http.createServer(<span class="function">(<span class="params">req, res</span>) =&gt;</span> &#123;</span><br><span class="line">    res.writeHead(<span class="number">200</span>);</span><br><span class="line">    res.end(<span class="string">&#x27;你好世界\n&#x27;</span>);</span><br><span class="line">  &#125;).listen(<span class="number">8000</span>);</span><br><span class="line"></span><br><span class="line">  <span class="built_in">console</span>.log(<span class="string">`工作进程 <span class="subst">$&#123;process.pid&#125;</span> 已启动`</span>);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<h3 id="13-3-2-入口"><a href="#13-3-2-入口" class="headerlink" title="13.3.2 入口"></a>13.3.2 入口</h3><p><code>cluster</code>模块的入口在<code>/lib/cluster.js</code>，这里的代码很简单：</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">&#x27;use strict&#x27;</span>;</span><br><span class="line"><span class="keyword">const</span> childOrMaster = <span class="string">&#x27;NODE_UNIQUE_ID&#x27;</span> <span class="keyword">in</span> process.env ? <span class="string">&#x27;child&#x27;</span> : <span class="string">&#x27;master&#x27;</span>;</span><br><span class="line"><span class="built_in">module</span>.exports = <span class="built_in">require</span>(<span class="string">`internal/cluster/<span class="subst">$&#123;childOrMaster&#125;</span>`</span>);</span><br></pre></td></tr></table></figure>

<p>可以看到，如果进程对象的环境变量中有<code>NODE_UNIQUE_ID</code>这个变量，就透传<code>internal/cluster/child.js</code>模块的输出，否则就透传<code>internal/cluster/master.js</code>模块的输出。这是<code>node</code>的主进程在进行子进程管理时的标识，后面的代码中可以看到当调用<code>cluster.fork( )</code>生成一个子进程时会以一个自增ID的形式生成这个环境变量。</p>
<h3 id="13-3-3-主进程模块master-js"><a href="#13-3-3-主进程模块master-js" class="headerlink" title="13.3.3 主进程模块master.js"></a>13.3.3 主进程模块master.js</h3><p>首先运行<code>node</code>程序的肯定是主线程，那么我们从<code>master.js</code>这个模块开始，先用工具折叠一下代码浏览一下：</p>
<p><img data-src="https://img2018.cnblogs.com/blog/1354575/201906/1354575-20190601090509233-278998873.png" alt="img"></p>
<p>可以看到除了模块属性外，cluster模块对外暴露的方法只有下面3个，其他的都是用来完成内部功能的：</p>
<ul>
<li><code>setupMaster(options )</code>-修改<code>fork</code>时默认设置</li>
<li><code>fork( )</code>-生成子进程</li>
<li><code>disconnect( )</code>- 断开和所有子进程的连接</li>
</ul>
<p>我们按照官方示例的逻辑路线来阅读代码<code>cluster.fork( )</code>方法定义在161-217行，一样是用折叠工具来看全貌：</p>
<p><img data-src="https://img2018.cnblogs.com/blog/1354575/201906/1354575-20190601090521479-130930914.png" alt="img"></p>
<p>可以看到<code>cluster.fork( )</code>执行时做了如下几件事情：</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="number">1.</span>设置主线程参数</span><br><span class="line"><span class="number">2.</span>传入一个自增参数id(就是前文提到的NODE_UNIQUE_ID)和环境信息env来生成一个worker线程的process对象</span><br><span class="line"><span class="number">3.</span>将id和新的process对象传入Worker构造器生成新的worker进程实例</span><br><span class="line"><span class="number">4.</span>在子进程的process对象上添加了一些事件监听</span><br><span class="line"><span class="number">5.</span>在cluster.workers中以id为键添加对子进程的引用</span><br><span class="line"><span class="number">6.</span>返回子进程worker实例</span><br></pre></td></tr></table></figure>

<p>接着看第一步<code>setupMaster( )</code>，在源码中50-95行，着重看81-95行：</p>
<p><img data-src="https://img2018.cnblogs.com/blog/1354575/201906/1354575-20190601090532829-2120517530.png" alt="img"></p>
<p>留意一下主线程在进程层面监听的<code>internalMessage</code>事件非常关键，主进程监听到这个事件后，首先判断消息对象的cmd属性是否为<code>NODE_DEBUGE_ENABLED</code>，并以此为条件判断后续语句是否执行，后续的逻辑是遍历每一个<code>worker</code>进程实例，如果子进程的状态是<code>online</code>或<code>listening</code>就将子进程<strong>pid</strong>作为参数调用主进程的<code>_debugProcess( )</code>方法，否则改为在<code>worker</code>进程实例首次上线时调用。</p>
<p><code>process._debugProcess</code>的定义在<code>src/node_process_methods.cc</code>里，看名字推测大致的意思就是为了启用对子进程的调试功能。这是一个重载方法，在<strong>windows</strong>和<strong>linux</strong>下有不同的实现。<code>linux</code>下的代码较短，基本可以看懂（不秀一下怎么对得住自己看1周的C++）：</p>
<figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#<span class="meta-keyword">ifdef</span> __POSIX__</span></span><br><span class="line"><span class="function"><span class="keyword">static</span> <span class="keyword">void</span> <span class="title">DebugProcess</span><span class="params">(<span class="keyword">const</span> FunctionCallbackInfo&lt;Value&gt;&amp; args)</span> </span>&#123;</span><br><span class="line">    <span class="comment">//这里的常量参数是通过地址引用的worker.process.pid</span></span><br><span class="line">  Environment* env = Environment::GetCurrent(args); </span><br><span class="line">    <span class="comment">//用pid做参数获取当前激活的环境变量，这一步应该是在获取上下文</span></span><br><span class="line"></span><br><span class="line">  <span class="keyword">if</span> (args.Length() != <span class="number">1</span>) &#123;<span class="comment">//不合法调用时报错，没什么可说的</span></span><br><span class="line">    <span class="keyword">return</span> env-&gt;ThrowError(<span class="string">&quot;Invalid number of arguments.&quot;</span>);</span><br><span class="line">  &#125;</span><br><span class="line">  </span><br><span class="line">  CHECK(args[<span class="number">0</span>]-&gt;IsNumber());<span class="comment">//检测参数</span></span><br><span class="line">  <span class="keyword">pid_t</span> pid = args[<span class="number">0</span>].As&lt;Integer&gt;()-&gt;Value();</span><br><span class="line">  <span class="keyword">int</span> r = kill(pid, SIGUSR1);<span class="comment">//发送SIGUSR1信号，终止了这个子进程</span></span><br><span class="line"></span><br><span class="line">  <span class="keyword">if</span> (r != <span class="number">0</span>) &#123;<span class="comment">//exit code为0时是正常退出，子进程未能正常中止时报错</span></span><br><span class="line">    <span class="keyword">return</span> env-&gt;ThrowErrnoException(errno, <span class="string">&quot;kill&quot;</span>);</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>win32平台中对应的代码比较长，看不懂。总结一下这里就是，在没有收到<code>cmd</code>属性等于<code>NODE_DEBUG_ENABLED</code>的内部消息之前，什么都不做，如果收到这个消息，就<strong>终止所有的子进程，或者通过事件在子进程第一次处于online状态就终止它</strong>。</p>
<p>按照执行顺序接下来是101-140行的<code>createWorkerProcess(id,env)</code>方法，看名字就知道是生成子进程process对象的，前半部分合并和处理环境参数，然后判断运行参数中是否包含启用<code>--inspect</code>功能的参数并进行一些处理，最后传入一堆参数调用了<code>fork</code>方法，这个方法就是<code>child_process.fork( )</code>，它就是用来生成子进程的，返回值就是子进程实例，你可以先简单浏览一下API<a target="_blank" rel="noopener" href="http://nodejs.cn/api/child_process.html#child_process_child_process_fork_modulepath_args_options">【官方文档child_process.fork功能】</a>，或者知道这里生成了子进程就好。</p>
<p>回到<code>cluster.fork</code>方法继续执行，下一步使用新生成的子进程process对象和唯一id作为参数传入Worker构造函数，生成<code>worker</code>实例,<code>Worker</code>的定义就在当前文件夹的<code>worker.js</code>中，它首先继承了<code>EventEmitter</code>的消息的发布订阅能力，然后把子进程的process对象挂在在自己的process属性上，接着为子进程添加<code>error</code>和 <code>message</code>事件的监听，最后暴露了一些更语义化的针对进程实例的管理方法（更详细的分析可以参考本系列前一篇博文）。生成了<code>worker</code>进程实例后，添加了对于<code>message</code>事件的响应，并在子进程<code>process</code>对象上监听进程的<code>exit</code>,<code>disconnect</code>,<code>internalMessage</code>事件，最后将worker实例和自己的id以键值对的形式添加到<code>cluster.workers</code>中记录，并通过<code>return</code>返回给外界，至此<code>master</code>模块的初始化流程就告一段落，先mark一下，后面还会讲这里。</p>
<h3 id="13-3-4-子进程模块child-js"><a href="#13-3-4-子进程模块child-js" class="headerlink" title="13.3.4 子进程模块child.js"></a>13.3.4 子进程模块child.js</h3><p>子进程模块是从<code>master.js</code>调用<code>child_process</code>时启动的，它和主进程是并行执行的。老规矩，代码折叠看一下：</p>
<p><img data-src="https://img2018.cnblogs.com/blog/1354575/201906/1354575-20190601090551857-551223998.png" alt="img"></p>
<p>看出什么了吗？<code>child.js</code>的代码里只有引用和定义，<code>_setupWorker</code>是在<code>nodejs</code>工作进程初始化时执行的，它在自己的独立进程中初始化了一个进程管理实例，并执行了下述逻辑：</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="number">1.</span>实例化进程管理对象worker</span><br><span class="line"><span class="number">2.</span>全局添加<span class="string">`disconnect`</span>事件响应</span><br><span class="line"><span class="number">3.</span>全局添加<span class="string">`internalMessage`</span>事件响应，主要是分发<span class="string">`act:newconn`</span>和<span class="string">`act:disconnect`</span>事件</span><br><span class="line"><span class="number">4.</span>用send方法发送<span class="string">`online`</span>事件，通知主线程自己已上线。</span><br></pre></td></tr></table></figure>

<p>注意，这个<code>process</code>对象就是IPC(Inter Process Communication,也称为跨进程通讯)能够实现的关键，很明显它继承了<code>EventEmitter</code>的消息收发能力，在子进程内部进行消息收发不存在任何问题，还记得<code>master.js</code>中<code>fork</code>方法吗？这个process就是调用<code>child_process</code>启动子进程时返回给主进程的那个process对象，当你在主进程中获取它后，就可以共享worker进程的消息能力，从而在资源隔离的条件下实现<code>master</code>和<code>worker</code>进程的跨进程通讯。<code>_getServer( )</code>方法是在建立server实例时调用的，等到驱动事件信息到达<code>child.js</code>时再看，可以留意一下最后两个添加在<code>Worker</code>原型方法上的方法，它们只在子进程中有效。</p>
<h2 id="13-4-小结"><a href="#13-4-小结" class="headerlink" title="13.4 小结"></a>13.4 小结</h2><p>至此，你已经看到node是如何通过cluster模块实现多实例并初始化跨进程通讯了。但是跨进程通讯的底层实现以及服务器的建立，以及如何在进程间协调网络请求的处理，还依赖于<code>net</code>和<code>http</code>的一些内容，只好等研究完了再继续，硬刚反正我是吃不消的。</p>
<h1 id="14-流机制"><a href="#14-流机制" class="headerlink" title="14.流机制"></a>14.流机制</h1><p>相信很多人对 Node 的 Stream 已经不陌生了，不论是请求流、响应流、文件流还是 socket 流，这些流的底层都是使用 <code>stream</code> 模块封装的，甚至我们平时用的最多的 <code>console.log</code> 打印日志也使用了它，不信你打开 Node runtime 的源码，看看 <a target="_blank" rel="noopener" href="https://github.com/nodejs/node/blob/master/lib/console.js#L82-L109"><code>lib/console.js</code></a>：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line">function write(ignoreErrors, stream, string, errorhandler) &#123;</span><br><span class="line">  &#x2F;&#x2F; ...</span><br><span class="line">  stream.once(&#39;error&#39;, noop);</span><br><span class="line">  stream.write(string, errorhandler);</span><br><span class="line">  &#x2F;&#x2F;...</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">Console.prototype.log &#x3D; function log(...args) &#123;</span><br><span class="line">  write(this._ignoreErrors,</span><br><span class="line">        this._stdout,</span><br><span class="line">        &#96;$&#123;util.format.apply(null, args)&#125;\n&#96;,</span><br><span class="line">        this._stdoutErrorHandler);</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure>

<p>Stream 模块做了很多事情，了解了 Stream，那么 Node 中其他很多模块理解起来就顺畅多了。</p>
<blockquote>
<p>本文代码和图片可以在这里取用：<a target="_blank" rel="noopener" href="https://github.com/barretlee/dive-into-node-stream%E3%80%82">https://github.com/barretlee/dive-into-node-stream。</a></p>
</blockquote>
<h2 id="14-1-stream-模块"><a href="#14-1-stream-模块" class="headerlink" title="14.1 stream 模块"></a>14.1 stream 模块</h2><p>如果你了解 <a target="_blank" rel="noopener" href="https://zh.wikipedia.org/zh-hans/%E7%94%9F%E4%BA%A7%E8%80%85%E6%B6%88%E8%B4%B9%E8%80%85%E9%97%AE%E9%A2%98">生产者和消费者问题</a> 的解法，那理解 stream 就基本没有压力了，它不仅仅是资料的起点和落点，还包含了一系列状态控制，可以说一个 stream 就是一个状态管理单元。了解内部机制的最佳方式除了看 <a target="_blank" rel="noopener" href="https://nodejs.org/api/stream.html">Node 官方文档</a>，还可以去看看 Node 的 <a target="_blank" rel="noopener" href="https://github.com/nodejs/node/blob/master/lib/">源码</a>：</p>
<ul>
<li><code>lib/module.js</code></li>
<li><code>lib/_stream_readable.js</code></li>
<li><code>lib/_stream_writable.js</code></li>
<li><code>lib/_stream_tranform.js</code></li>
<li><code>lib/_stream_duplex.js</code></li>
</ul>
<p>把 <code>Readable</code> 和 <code>Writable</code> 看明白，Tranform 和 Duplex 就不难理解了。</p>
<h2 id="14-2-Readable-Stream"><a href="#14-2-Readable-Stream" class="headerlink" title="14.2 Readable Stream"></a>14.2 Readable Stream</h2><p>Readable Stream 存在两种模式，一种是叫做 <code>Flowing Mode</code>，流动模式，在 Stream 上绑定 ondata 方法就会自动触发这个模式，比如：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">const readable &#x3D; getReadableStreamSomehow();</span><br><span class="line">readable.on(&#39;data&#39;, (chunk) &#x3D;&gt; &#123;</span><br><span class="line">  console.log(&#96;Received $&#123;chunk.length&#125; bytes of data.&#96;);</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure>

<p>这个模式的流程图如下：</p>
<p><img data-src="http://www.barretlee.com/blogimgs/2017/06/06/node-stream-readable-flowing.png" alt="Flowing"></p>
<p>资源的数据流并不是直接流向消费者，而是先 push 到缓存池，缓存池有一个水位标记 <code>highWatermark</code>，超过这个标记阈值，push 的时候会返回 <code>false</code>，什么场景下会出现这种情况呢？</p>
<ul>
<li>消费者主动执行了 <code>.pause()</code></li>
<li>消费速度比数据 push 到缓存池的生产速度慢</li>
</ul>
<p>有个专有名词来形成这种情况，叫做「背压」，Writable Stream 也存在类似的情况。</p>
<p>流动模式，这个名词还是很形象的，缓存池就像一个水桶，消费者通过管口接水，同时，资源池就像一个水泵，不断地往水桶中泵水，而 highWaterMark 是水桶的浮标，达到阈值就停止蓄水。下面是一个简单的 Demo：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br></pre></td><td class="code"><pre><span class="line">const Readable &#x3D; require(&#39;stream&#39;).Readable;</span><br><span class="line"></span><br><span class="line">&#x2F;&#x2F; Stream 实现</span><br><span class="line">class MyReadable extends Readable &#123;</span><br><span class="line">  constructor(dataSource, options) &#123;</span><br><span class="line">    super(options);</span><br><span class="line">    this.dataSource &#x3D; dataSource;</span><br><span class="line">  &#125;</span><br><span class="line">  &#x2F;&#x2F; 继承了 Readable 的类必须实现这个函数</span><br><span class="line">  &#x2F;&#x2F; 触发系统底层对流的读取</span><br><span class="line">  _read() &#123;</span><br><span class="line">    const data &#x3D; this.dataSource.makeData();</span><br><span class="line">    this.push(data);</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">&#x2F;&#x2F; 模拟资源池</span><br><span class="line">const dataSource &#x3D; &#123;</span><br><span class="line">  data: new Array(10).fill(&#39;-&#39;),</span><br><span class="line">  &#x2F;&#x2F; 每次读取时 pop 一个数据</span><br><span class="line">  makeData() &#123;</span><br><span class="line">    if (!dataSource.data.length) return null;</span><br><span class="line">    return dataSource.data.pop();</span><br><span class="line">  &#125;</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line">const myReadable &#x3D; new MyReadable(dataSource);</span><br><span class="line">myReadable.setEncoding(&#39;utf8&#39;);</span><br><span class="line">myReadable.on(&#39;data&#39;, (chunk) &#x3D;&gt; &#123;</span><br><span class="line">  console.log(chunk);</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure>

<p>另外一种模式是 <code>Non-Flowing Mode</code>，没流动，也就是暂停模式，这是 Stream 的预设模式，Stream 实例的 <code>_readableState.flow</code> 有三个状态，分别是：</p>
<ul>
<li><code>_readableState.flow = null</code>，暂时没有消费者过来</li>
<li><code>_readableState.flow = false</code>，主动触发了 <code>.pause()</code></li>
<li><code>_readableState.flow = true</code>，流动模式</li>
</ul>
<p>当我们监听了 onreadable 事件后，会进入这种模式，比如：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">const myReadable &#x3D; new MyReadable(dataSource);</span><br><span class="line">myReadable.setEncoding(&#39;utf8&#39;);</span><br><span class="line">myReadable.on(&#39;readable&#39;, () &#x3D;&gt; &#123;&#125;);</span><br></pre></td></tr></table></figure>

<p>监听 <code>readable</code> 的回调函数第一个参数不会传递内容，需要我们通过 <code>myReadable.read()</code> 主动读取，为啥呢，可以看看下面这张图：</p>
<p><img data-src="http://www.barretlee.com/blogimgs/2017/06/06/node-stream-non-flowing.png" alt="node-stream-non-flowing"></p>
<p>资源池会不断地往缓存池输送数据，直到 highWaterMark 阈值，消费者监听了 readable 事件并不会消费数据，需要主动调用 <code>.read([size])</code> 函数才会从缓存池取出，并且可以带上 size 参数，用多少就取多少：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">const myReadable &#x3D; new MyReadable(dataSource);</span><br><span class="line">myReadable.setEncoding(&#39;utf8&#39;);</span><br><span class="line">myReadable.on(&#39;readable&#39;, () &#x3D;&gt; &#123;</span><br><span class="line">  let chunk;</span><br><span class="line">  while (null !&#x3D;&#x3D; (chunk &#x3D; myReadable.read())) &#123;</span><br><span class="line">    console.log(&#96;Received $&#123;chunk.length&#125; bytes of data.&#96;);</span><br><span class="line">  &#125;</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure>

<p>这里需要注意一点，只要数据达到缓存池都会触发一次 readable 事件，有可能出现「消费者正在消费数据的时候，又触发了一次 readable 事件，那么下次回调中 read 到的数据可能为空」的情况。我们可以通过 <code>_readableState.buffer</code> 来查看缓存池到底缓存了多少资源：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">let once &#x3D; false;</span><br><span class="line">myReadable.on(&#39;readable&#39;, (chunk) &#x3D;&gt; &#123;</span><br><span class="line">  console.log(myReadable._readableState.buffer.length);</span><br><span class="line">  if (once) return;</span><br><span class="line">  once &#x3D; true;</span><br><span class="line">  console.log(myReadable.read());</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure>

<p>上面的代码我们只消费一次缓存池的数据，那么在消费后，缓存池又收到了一次资源池的 push 操作，此时还会触发一次 readable 事件，我们可以看看这次存了多大的 buffer。</p>
<p>需要注意的是，buffer 大小也是有上限的，默认设置为 16kb，也就是 16384 个字节长度，它最大可设置为 8Mb，没记错的话，这个值好像是 Node 的 new space memory 的大小。</p>
<p>上面介绍了 Readable Stream 大概的机制，还有很多细节部分没有提到，比如 <code>Flowing Mode</code> 在不同 Node 版本中的 Stream 实现不太一样，实际上，它有三个版本，上面提到的是第 2 和 第 3 个版本的实现；再比如 <code>Mixins Mode</code> 模式，一般我们只推荐（允许）使用 ondata 和 onreadable 的一种来处理 Readable Stream，但是如果要求在 <code>Non-Flowing Mode</code> 的情况下使用 ondata 如何实现呢？那么就可以考虑 <code>Mixins Mode</code> 了。</p>
<h2 id="14-3-Writable-Stream"><a href="#14-3-Writable-Stream" class="headerlink" title="14.3 Writable Stream"></a>14.3 Writable Stream</h2><p>原理与 Readable Stream 是比较相似的，数据流过来的时候，会直接写入到资源池，当写入速度比较缓慢或者写入暂停时，数据流会进入队列池缓存起来，如下图所示：</p>
<p><img data-src="http://www.barretlee.com/blogimgs/2017/06/06/node-stream-writable.png" alt="node-stream-writable"></p>
<p>当生产者写入速度过快，把队列池装满了之后，就会出现「背压」，这个时候是需要告诉生产者暂停生产的，当队列释放之后，Writable Stream 会给生产者发送一个 <code>drain</code> 消息，让它恢复生产。下面是一个写入一百万条数据的 Demo：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line">function writeOneMillionTimes(writer, data, encoding, callback) &#123;</span><br><span class="line">  let i &#x3D; 10000;</span><br><span class="line">  write();</span><br><span class="line">  function write() &#123;</span><br><span class="line">    let ok &#x3D; true;</span><br><span class="line">    while(i-- &gt; 0 &amp;&amp; ok) &#123;</span><br><span class="line">      &#x2F;&#x2F; 写入结束时回调</span><br><span class="line">      ok &#x3D; writer.write(data, encoding, i &#x3D;&#x3D;&#x3D; 0 ? callback : null);</span><br><span class="line">    &#125;</span><br><span class="line">    if (i &gt; 0) &#123;</span><br><span class="line">      &#x2F;&#x2F; 这里提前停下了，&#39;drain&#39; 事件触发后才可以继续写入  </span><br><span class="line">      console.log(&#39;drain&#39;, i);</span><br><span class="line">      writer.once(&#39;drain&#39;, write);</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>我们构造一个 Writable Stream，在写入到资源池的时候，我们稍作处理，让它效率低一点：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line">const Writable &#x3D; require(&#39;stream&#39;).Writable;</span><br><span class="line">const writer &#x3D; new Writable(&#123;</span><br><span class="line">  write(chunk, encoding, callback) &#123;</span><br><span class="line">    &#x2F;&#x2F; 比 process.nextTick() 稍慢</span><br><span class="line">    setTimeout(() &#x3D;&gt; &#123;</span><br><span class="line">      callback &amp;&amp; callback();</span><br><span class="line">    &#125;);</span><br><span class="line">  &#125;</span><br><span class="line">&#125;);</span><br><span class="line"></span><br><span class="line">writeOneMillionTimes(writer, &#39;simple&#39;, &#39;utf8&#39;, () &#x3D;&gt; &#123;</span><br><span class="line">  console.log(&#39;end&#39;);</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure>

<p>最后执行的结果是：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">drain 7268</span><br><span class="line">drain 4536</span><br><span class="line">drain 1804</span><br><span class="line">end</span><br></pre></td></tr></table></figure>

<p>说明程序遇到了三次「背压」，如果我们没有在上面绑定 <code>writer.once(&#39;drain&#39;)</code>，那么最后的结果就是 Stream 将第一次获取的数据消耗完变结束了程序。</p>
<h2 id="14-4-pipe"><a href="#14-4-pipe" class="headerlink" title="14.4 pipe"></a>14.4 pipe</h2><p>了解了 Readable 和 Writable，pipe 这个常用的函数应该就很好理解了，</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">readable.pipe(writable);</span><br></pre></td></tr></table></figure>

<p>这句代码的语意性很强，readable 通过 pipe（管道）传输给 writable，pipe 的实现大致如下（伪代码）：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line">Readable.prototype.pipe &#x3D; function(writable, options) &#123;</span><br><span class="line">  this.on(&#39;data&#39;, (chunk) &#x3D;&gt; &#123;</span><br><span class="line">    let ok &#x3D; writable.write(chunk);</span><br><span class="line">true&#x2F;&#x2F; 背压，暂停</span><br><span class="line">    !ok &amp;&amp; this.pause();</span><br><span class="line">  &#125;);</span><br><span class="line">  writable.on(&#39;drain&#39;, () &#x3D;&gt; &#123;</span><br><span class="line">    &#x2F;&#x2F; 恢复</span><br><span class="line">    this.resume();</span><br><span class="line">  &#125;);</span><br><span class="line">  &#x2F;&#x2F; 告诉 writable 有流要导入</span><br><span class="line">  writable.emit(&#39;pipe&#39;, this);</span><br><span class="line">  &#x2F;&#x2F; 支持链式调用</span><br><span class="line">  return writable;</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure>

<p>上面做了五件事情：</p>
<ul>
<li><code>emit(pipe)</code>，通知写入</li>
<li><code>.write()</code>，新数据过来，写入</li>
<li><code>.pause()</code>，消费者消费速度慢，暂停写入</li>
<li><code>.resume()</code>，消费者完成消费，继续写入</li>
<li><code>return writable</code>，支持链式调用</li>
</ul>
<p>当然，上面只是最简单的逻辑，还有很多异常和临界判断没有加入，具体可以去看看 Node 的代码（ <a target="_blank" rel="noopener" href="https://github.com/nodejs/node/blob/master/lib/_stream_readable.js#L541-L684">/lib/_stream_readable.js</a>）。</p>
<h2 id="14-5-Duplex-Stream"><a href="#14-5-Duplex-Stream" class="headerlink" title="14.5 Duplex Stream"></a>14.5 Duplex Stream</h2><p>Duplex，双工的意思，它的输入和输出可以没有任何关系，</p>
<p><img data-src="http://www.barretlee.com/blogimgs/2017/06/06/node-stream-duplex.png" alt="Node-Stream-Duplex"></p>
<p>Duplex Stream 实现特别简单，不到一百行代码，它继承了 Readable Stream，并拥有 Writable Stream 的方法（<a target="_blank" rel="noopener" href="https://github.com/nodejs/node/blob/master/lib/_stream_duplex.js#L31-L42">源码地址</a>）：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line">const util &#x3D; require(&#39;util&#39;);</span><br><span class="line">const Readable &#x3D; require(&#39;_stream_readable&#39;);</span><br><span class="line">const Writable &#x3D; require(&#39;_stream_writable&#39;);</span><br><span class="line"></span><br><span class="line">util.inherits(Duplex, Readable);</span><br><span class="line"></span><br><span class="line">var keys &#x3D; Object.keys(Writable.prototype);</span><br><span class="line">for (var v &#x3D; 0; v &lt; keys.length; v++) &#123;</span><br><span class="line">  var method &#x3D; keys[v];</span><br><span class="line">  if (!Duplex.prototype[method])</span><br><span class="line">    Duplex.prototype[method] &#x3D; Writable.prototype[method];</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>我们可以通过 options 参数来配置它为只可读、只可写或者半工模式，一个简单的 Demo：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line">var Duplex &#x3D; require(&#39;stream&#39;).Duplex</span><br><span class="line"></span><br><span class="line">const duplex &#x3D; Duplex();</span><br><span class="line"></span><br><span class="line">&#x2F;&#x2F; readable</span><br><span class="line">let i &#x3D; 2;</span><br><span class="line">duplex._read &#x3D; function () &#123;</span><br><span class="line">  this.push(i-- ? &#39;read &#39; + i : null);</span><br><span class="line">&#125;;</span><br><span class="line">duplex.on(&#39;data&#39;, data &#x3D;&gt; console.log(data.toString()));</span><br><span class="line"></span><br><span class="line">&#x2F;&#x2F; writable</span><br><span class="line">duplex._write &#x3D; function (chunk, encoding, callback) &#123;</span><br><span class="line">  console.log(chunk.toString());</span><br><span class="line">  callback();</span><br><span class="line">&#125;;</span><br><span class="line">duplex.write(&#39;write&#39;);</span><br></pre></td></tr></table></figure>

<p>输出的结果为：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">write</span><br><span class="line">read 1</span><br><span class="line">read 0</span><br></pre></td></tr></table></figure>

<p>可以看出，两个管道是相互之间不干扰的。</p>
<h2 id="14-6-Transform-Stream"><a href="#14-6-Transform-Stream" class="headerlink" title="14.6 Transform Stream"></a>14.6 Transform Stream</h2><p>Transform Stream 集成了 Duplex Stream，它同样具备 Readable 和 Writable 的能力，只不过它的输入和输出是存在相互关联的，中间做了一次转换处理。常见的处理有 Gzip 压缩、解压等。</p>
<p><img data-src="http://www.barretlee.com/blogimgs/2017/06/06/node-stream-transform.png" alt="node-stream-transform"></p>
<p>Transform 的处理就是通过 <code>_transform</code> 函数将 Duplex 的 Readable 连接到 Writable，由于 Readable 的生产效率与 Writable 的消费效率是一样的，所以这里 Transform 内部不存在「背压」问题，背压问题的源头是外部的生产者和消费者速度差造成的。</p>
<p>关于 Transfrom Stream，我写了一个简单的 Demo：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><span class="line">const Transform &#x3D; require(&#39;stream&#39;).Transform;</span><br><span class="line">const MAP &#x3D; &#123;</span><br><span class="line">  &#39;Barret&#39;: &#39;靖&#39;,</span><br><span class="line">  &#39;Lee&#39;: &#39;李&#39;</span><br><span class="line">&#125;;</span><br><span class="line">  </span><br><span class="line">class Translate extends Transform &#123;</span><br><span class="line">  constructor(dataSource, options) &#123;</span><br><span class="line">    super(options);</span><br><span class="line">  &#125;</span><br><span class="line">  _transform(buf, enc, next) &#123;</span><br><span class="line">    const key &#x3D; buf.toString();</span><br><span class="line">    const data &#x3D; MAP[key];</span><br><span class="line">    this.push(data);</span><br><span class="line">    next();</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">var transform &#x3D; new Translate();</span><br><span class="line">transform.on(&#39;data&#39;, data &#x3D;&gt; console.log(data.toString()));</span><br><span class="line">transform.write(&#39;Lee&#39;);</span><br><span class="line">transform.write(&#39;Barret&#39;);</span><br><span class="line">transform.end();</span><br></pre></td></tr></table></figure>

<h2 id="小结"><a href="#小结" class="headerlink" title="小结"></a>小结</h2><p>本文主要参考和查阅 Node 官网的文档和源码，细节问题都是从源码中找到的答案，如有理解不准确之处，还请斧正。关于 Stream，这篇文章只是讲述了基础的原理，还有很多细节之处没有讲到，要真正理解它，还是需要多读读文档，写写代码。</p>
<p>了解了这些 Stream 的内部机制，对我们后续深入理解上层代码有很大的促进作用，特别希望初学 Node 的同学花点时间进来看看。</p>
<p>另外，本文代码和图片可以在这里取用：<a target="_blank" rel="noopener" href="https://github.com/barretlee/dive-into-node-stream%E3%80%82">https://github.com/barretlee/dive-into-node-stream。</a></p>
<h1 id="15-pipe原理"><a href="#15-pipe原理" class="headerlink" title="15.pipe原理"></a>15.pipe原理</h1><p>通过流我们可以将一大块数据拆分为一小部分一点一点的流动起来，而无需一次性全部读入，在 Linux 下我们可以通过 | 符号实现，类似的在 Nodejs 的 Stream 模块中同样也为我们提供了 pipe() 方法来实现。</p>
<h2 id="15-1-Nodejs-Stream-pipe-基本示例"><a href="#15-1-Nodejs-Stream-pipe-基本示例" class="headerlink" title="15.1 Nodejs Stream pipe 基本示例"></a><strong>15.1 Nodejs Stream pipe 基本示例</strong></h2><p>选择 Koa 来实现这个简单的 Demo，因为之前有人在 “Nodejs技术栈” 交流群问过一个问题，怎么在 Koa 中返回一个 Stream，顺便在下文借此机会提下。</p>
<h3 id="15-1-1-未使用-Stream-pipe-情况"><a href="#15-1-1-未使用-Stream-pipe-情况" class="headerlink" title="15.1.1 未使用 Stream pipe 情况"></a><strong>15.1.1 未使用 Stream pipe 情况</strong></h3><p>在 Nodejs 中 I/O 操作都是异步的，先用 util 模块的 promisify 方法将 fs.readFile 的 callback 形式转为 Promise 形式，这块代码看似没问题，但是它的体验不是很好，因为它是将数据一次性读入内存再进行的返回，当数据文件很大的时候也是对内存的一种消耗，因此不推荐它。</p>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> Koa = <span class="built_in">require</span>(<span class="string">&#x27;koa&#x27;</span>);</span><br><span class="line"><span class="keyword">const</span> fs = <span class="built_in">require</span>(<span class="string">&#x27;fs&#x27;</span>);</span><br><span class="line"><span class="keyword">const</span> app = <span class="keyword">new</span> Koa();</span><br><span class="line"><span class="keyword">const</span> &#123; promisify &#125; = <span class="built_in">require</span>(<span class="string">&#x27;util&#x27;</span>);</span><br><span class="line"><span class="keyword">const</span> &#123; resolve &#125; = <span class="built_in">require</span>(<span class="string">&#x27;path&#x27;</span>);</span><br><span class="line"><span class="keyword">const</span> readFile = promisify(fs.readFile);</span><br><span class="line"></span><br><span class="line">app.use(<span class="keyword">async</span> ctx =&gt; &#123;</span><br><span class="line">  <span class="keyword">try</span> &#123;</span><br><span class="line">    ctx.body = <span class="keyword">await</span> readFile(resolve(__dirname, <span class="string">&#x27;test.json&#x27;</span>));</span><br><span class="line">  &#125; <span class="keyword">catch</span>(err) &#123; ctx.body = err &#125;;</span><br><span class="line">&#125;);</span><br><span class="line"></span><br><span class="line">app.listen(<span class="number">3000</span>);</span><br></pre></td></tr></table></figure>

<h3 id="15-1-2-使用-Stream-pipe-情况"><a href="#15-1-2-使用-Stream-pipe-情况" class="headerlink" title="15.1.2 使用 Stream pipe 情况"></a><strong>15.1.2 使用 Stream pipe 情况</strong></h3><p>下面，再看看怎么通过 Stream 的方式在 Koa 框架中响应数据</p>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">...</span><br><span class="line">app.use(<span class="keyword">async</span> ctx =&gt; &#123;</span><br><span class="line">  <span class="keyword">try</span> &#123;</span><br><span class="line">    <span class="keyword">const</span> readable = fs.createReadStream(resolve(__dirname, <span class="string">&#x27;test.json&#x27;</span>));</span><br><span class="line">    ctx.body = readable;</span><br><span class="line">  &#125; <span class="keyword">catch</span>(err) &#123; ctx.body = err &#125;;</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure>

<p>以上在 Koa 中直接创建一个可读流赋值给 ctx.body 就可以了，你可能疑惑了为什么没有 pipe 方法，因为框架给你封装好了，不要被表象所迷惑了，看下相关源码：</p>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// https://github.com/koajs/koa/blob/master/lib/application.js#L256</span></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">respond</span>(<span class="params">ctx</span>) </span>&#123;</span><br><span class="line">  ...</span><br><span class="line">  <span class="keyword">let</span> body = ctx.body;</span><br><span class="line">  <span class="keyword">if</span> (body <span class="keyword">instanceof</span> Stream) <span class="keyword">return</span> body.pipe(res);</span><br><span class="line">  ...</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>没有神奇之处，框架在返回的时候做了层判断，因为 res 是一个可写流对象，如果 body 也是一个 Stream 对象（此时的 Body 是一个可读流），则使用 body.pipe(res) 以流的方式进行响应。</p>
<h3 id="15-1-3-使用-Stream-VS-不使用-Stream"><a href="#15-1-3-使用-Stream-VS-不使用-Stream" class="headerlink" title="15.1.3 使用 Stream VS 不使用 Stream"></a><strong>15.1.3 使用 Stream VS 不使用 Stream</strong></h3><p>使用流：</p>
<p><img data-src="https://images2015.cnblogs.com/blog/561179/201701/561179-20170126172845566-1089400487.gif" alt="img"></p>
<p>不适用流：</p>
<p><img data-src="https://images2015.cnblogs.com/blog/561179/201701/561179-20170126170225816-1851442511.gif" alt="img"></p>
<h2 id="15-2-pipe-的调用过程与实现原理分析"><a href="#15-2-pipe-的调用过程与实现原理分析" class="headerlink" title="15.2 pipe 的调用过程与实现原理分析"></a><strong>15.2 pipe 的调用过程与实现原理分析</strong></h2><p>以上最后以流的方式响应数据最核心的实现就是使用 pipe 方法来实现的输入、输出，本节的重点也是研究 pipe 的实现，最好的打开方式通过阅读源码实现吧。</p>
<h3 id="15-2-1-顺藤摸瓜"><a href="#15-2-1-顺藤摸瓜" class="headerlink" title="15.2.1 顺藤摸瓜"></a><strong>15.2.1 顺藤摸瓜</strong></h3><p>在应用层我们调用了 fs.createReadStream() 这个方法，顺藤摸瓜找到这个方法创建的可读流对象的 pipe 方法实现，以下仅列举核心代码实现，基于 Nodejs v12.x 源码。</p>
<h4 id="15-2-1-1-lib-fs-js"><a href="#15-2-1-1-lib-fs-js" class="headerlink" title="15.2.1.1 /lib/fs.js"></a><strong>15.2.1.1 /lib/fs.js</strong></h4><p>导出一个 createReadStream 方法，在这个方法里面创建了一个 ReadStream 可读流对象，且 ReadStream 来自 internal/fs/streams 文件，继续向下找。</p>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// https://github.com/nodejs/node/blob/v12.x/lib/fs.js</span></span><br><span class="line"><span class="comment">// 懒加载，主要在用到的时候用来实例化 ReadStream、WriteStream ... 等对象</span></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">lazyLoadStreams</span>(<span class="params"></span>) </span>&#123;</span><br><span class="line">  <span class="keyword">if</span> (!ReadStream) &#123;</span><br><span class="line">    (&#123; ReadStream, WriteStream &#125; = <span class="built_in">require</span>(<span class="string">&#x27;internal/fs/streams&#x27;</span>));</span><br><span class="line">    [ FileReadStream, FileWriteStream ] = [ ReadStream, WriteStream ];</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">createReadStream</span>(<span class="params">path, options</span>) </span>&#123;</span><br><span class="line">  lazyLoadStreams();</span><br><span class="line">  <span class="keyword">return</span> <span class="keyword">new</span> ReadStream(path, options); <span class="comment">// 创建一个可读流</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="built_in">module</span>.exports = fs = &#123;</span><br><span class="line">  createReadStream, <span class="comment">// 导出 createReadStream 方法</span></span><br><span class="line">  ...</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<h4 id="15-2-1-2-lib-internal-fs-streams-js"><a href="#15-2-1-2-lib-internal-fs-streams-js" class="headerlink" title="15.2.1.2 /lib/internal/fs/streams.js"></a><strong>15.2.1.2 /lib/internal/fs/streams.js</strong></h4><p>这个方法里定义了构造函数 ReadStream，且在原型上定义了 open、_read、_destroy 等方法，并没有我们要找的 pipe 方法。</p>
<p>但是呢<strong>通过 ObjectSetPrototypeOf 方法实现了继承，ReadStream 继承了 Readable 在原型中定义的函数，接下来继续查找 Readable 的实现</strong>。</p>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// https://github.com/nodejs/node/blob/v12.x/lib/internal/fs/streams.js</span></span><br><span class="line"><span class="keyword">const</span> &#123; Readable, Writable &#125; = <span class="built_in">require</span>(<span class="string">&#x27;stream&#x27;</span>);</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">ReadStream</span>(<span class="params">path, options</span>) </span>&#123;</span><br><span class="line">  <span class="keyword">if</span> (!(<span class="built_in">this</span> <span class="keyword">instanceof</span> ReadStream))</span><br><span class="line">    <span class="keyword">return</span> <span class="keyword">new</span> ReadStream(path, options);</span><br><span class="line"></span><br><span class="line">  ...</span><br><span class="line">  Readable.call(<span class="built_in">this</span>, options);</span><br><span class="line">  ...</span><br><span class="line">&#125;</span><br><span class="line">ObjectSetPrototypeOf(ReadStream.prototype, Readable.prototype);</span><br><span class="line">ObjectSetPrototypeOf(ReadStream, Readable);</span><br><span class="line"></span><br><span class="line">ReadStream.prototype.open = <span class="function"><span class="keyword">function</span>(<span class="params"></span>) </span>&#123; ... &#125;;</span><br><span class="line"></span><br><span class="line">ReadStream.prototype._read = <span class="function"><span class="keyword">function</span>(<span class="params">n</span>) </span>&#123; ... &#125;;;</span><br><span class="line"></span><br><span class="line">ReadStream.prototype._destroy = <span class="function"><span class="keyword">function</span>(<span class="params">err, cb</span>) </span>&#123; ... &#125;;</span><br><span class="line">...</span><br><span class="line"></span><br><span class="line"><span class="built_in">module</span>.exports = &#123;</span><br><span class="line">  ReadStream,</span><br><span class="line">  WriteStream</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure>

<h4 id="15-2-1-3-lib-stream-js"><a href="#15-2-1-3-lib-stream-js" class="headerlink" title="15.2.1.3 /lib/stream.js"></a><strong>15.2.1.3 /lib/stream.js</strong></h4><p>在 stream.js 的实现中，有条注释：**在 Readable/Writable/Duplex/… 之前导入 Stream，原因是为了避免 cross-reference(require)**，为什么会这样？</p>
<p>第一步 stream.js 这里将 require(‘internal/streams/legacy’) 导出复制给了 Stream。</p>
<p>在之后的 _stream_readable、Writable、Duplex … 模块也会反过来引用 stream.js 文件，具体实现下面会看到。</p>
<p>Stream 导入了 internal/streams/legacy</p>
<p><strong>上面 /lib/internal/fs/streams.js 文件从 stream 模块获取了一个 Readable 对象，就是下面的 Stream.Readable 的定义。</strong></p>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// https://github.com/nodejs/node/blob/v12.x/lib/stream.js</span></span><br><span class="line"><span class="comment">// Note: export Stream before Readable/Writable/Duplex/...</span></span><br><span class="line"><span class="comment">// to avoid a cross-reference(require) issues</span></span><br><span class="line"><span class="keyword">const</span> Stream = <span class="built_in">module</span>.exports = <span class="built_in">require</span>(<span class="string">&#x27;internal/streams/legacy&#x27;</span>);</span><br><span class="line"></span><br><span class="line">Stream.Readable = <span class="built_in">require</span>(<span class="string">&#x27;_stream_readable&#x27;</span>);</span><br><span class="line">Stream.Writable = <span class="built_in">require</span>(<span class="string">&#x27;_stream_writable&#x27;</span>);</span><br><span class="line">Stream.Duplex = <span class="built_in">require</span>(<span class="string">&#x27;_stream_duplex&#x27;</span>);</span><br><span class="line">Stream.Transform = <span class="built_in">require</span>(<span class="string">&#x27;_stream_transform&#x27;</span>);</span><br><span class="line">Stream.PassThrough = <span class="built_in">require</span>(<span class="string">&#x27;_stream_passthrough&#x27;</span>);</span><br><span class="line">...</span><br></pre></td></tr></table></figure>

<h4 id="15-2-1-4-lib-internal-streams-legacy-js"><a href="#15-2-1-4-lib-internal-streams-legacy-js" class="headerlink" title="15.2.1.4 /lib/internal/streams/legacy.js"></a><strong>15.2.1.4 /lib/internal/streams/legacy.js</strong></h4><p>上面的 Stream 等于 internal/streams/legacy，首先继承了 Events 模块，之后呢在原型上定义了 pipe 方法，刚开始看到这里的时候以为实现是在这里了，但后来看 _stream_readable 的实现之后，发现 _stream_readable 继承了 Stream 之后自己又重新实现了 pipe 方法，那么疑问来了这个模块的 pipe 方法是干嘛的？什么时候会被用？翻译文件名 “legacy=遗留”？有点没太理解，难道是遗留了？有清楚的大佬可以指点下，也欢迎在公众号 “Nodejs技术栈” 后台加我微信一块讨论下！</p>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// https://github.com/nodejs/node/blob/v12.x/lib/internal/streams/legacy.js</span></span><br><span class="line"><span class="keyword">const</span> &#123;</span><br><span class="line">  ObjectSetPrototypeOf,</span><br><span class="line">&#125; = primordials;</span><br><span class="line"><span class="keyword">const</span> EE = <span class="built_in">require</span>(<span class="string">&#x27;events&#x27;</span>);</span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">Stream</span>(<span class="params">opts</span>) </span>&#123;</span><br><span class="line">  EE.call(<span class="built_in">this</span>, opts);</span><br><span class="line">&#125;</span><br><span class="line">ObjectSetPrototypeOf(Stream.prototype, EE.prototype);</span><br><span class="line">ObjectSetPrototypeOf(Stream, EE);</span><br><span class="line"></span><br><span class="line">Stream.prototype.pipe = <span class="function"><span class="keyword">function</span>(<span class="params">dest, options</span>) </span>&#123;</span><br><span class="line">  ...</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line"><span class="built_in">module</span>.exports = Stream;</span><br></pre></td></tr></table></figure>

<h4 id="15-2-1-5-lib-stream-readable-js"><a href="#15-2-1-5-lib-stream-readable-js" class="headerlink" title="15.2.1.5 /lib/_stream_readable.js"></a><strong>15.2.1.5 /lib/_stream_readable.js</strong></h4><p>在 _stream_readable.js 的实现里面定义了 Readable 构造函数，且继承于 Stream，这个 Stream 正是我们上面提到的 /lib/stream.js 文件，而在 /lib/stream.js 文件里加载了 internal/streams/legacy 文件且重写了里面定义的 pipe 方法。</p>
<p>经过上面一系列的分析，终于找到可读流的 pipe 在哪里，同时也更进一步的认识到了在创建一个可读流时的执行调用过程，下面将重点来看这个方法的实现。</p>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">module</span>.exports = Readable;</span><br><span class="line">Readable.ReadableState = ReadableState;</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> EE = <span class="built_in">require</span>(<span class="string">&#x27;events&#x27;</span>);</span><br><span class="line"><span class="keyword">const</span> Stream = <span class="built_in">require</span>(<span class="string">&#x27;stream&#x27;</span>);</span><br><span class="line"></span><br><span class="line">ObjectSetPrototypeOf(Readable.prototype, Stream.prototype);</span><br><span class="line">ObjectSetPrototypeOf(Readable, Stream);</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">Readable</span>(<span class="params">options</span>) </span>&#123;</span><br><span class="line">  <span class="keyword">if</span> (!(<span class="built_in">this</span> <span class="keyword">instanceof</span> Readable))</span><br><span class="line">    <span class="keyword">return</span> <span class="keyword">new</span> Readable(options);</span><br><span class="line"></span><br><span class="line">  ...</span><br><span class="line">  Stream.call(<span class="built_in">this</span>, options); <span class="comment">// 继承自 Stream 构造函数的定义</span></span><br><span class="line">&#125;</span><br><span class="line">...</span><br></pre></td></tr></table></figure>

<h3 id="15-2-2-stream-readable-实现分析"><a href="#15-2-2-stream-readable-实现分析" class="headerlink" title="15.2.2 _stream_readable 实现分析"></a><strong>15.2.2 _stream_readable 实现分析</strong></h3><h4 id="15-2-2-1-声明构造函数-Readable"><a href="#15-2-2-1-声明构造函数-Readable" class="headerlink" title="15.2.2.1 声明构造函数 Readable"></a><strong>15.2.2.1 声明构造函数 Readable</strong></h4><p>声明构造函数 Readable 继承 Stream 的构造函数和原型。</p>
<p>Stream 是 /lib/stream.js 文件，上面分析了，这个文件继承了 events 事件，此时也就拥有了 events 在原型中定义的属性，例如 on、emit 等方法。</p>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> Stream = <span class="built_in">require</span>(<span class="string">&#x27;stream&#x27;</span>);</span><br><span class="line">ObjectSetPrototypeOf(Readable.prototype, Stream.prototype);</span><br><span class="line">ObjectSetPrototypeOf(Readable, Stream);</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">Readable</span>(<span class="params">options</span>) </span>&#123;</span><br><span class="line">  <span class="keyword">if</span> (!(<span class="built_in">this</span> <span class="keyword">instanceof</span> Readable))</span><br><span class="line">    <span class="keyword">return</span> <span class="keyword">new</span> Readable(options);</span><br><span class="line"></span><br><span class="line">  ...</span><br><span class="line"></span><br><span class="line">  Stream.call(<span class="built_in">this</span>, options);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<h4 id="15-2-2-2-声明-pipe-方法，订阅-data-事件"><a href="#15-2-2-2-声明-pipe-方法，订阅-data-事件" class="headerlink" title="15.2.2.2 声明 pipe 方法，订阅 data 事件"></a><strong>15.2.2.2 声明 pipe 方法，订阅 data 事件</strong></h4><p>在 Stream 的原型上声明 pipe 方法，订阅 data 事件，src 为可读流对象，dest 为可写流对象。</p>
<p>我们在使用 pipe 方法的时候也是监听的 data 事件，一边读取数据一边写入数据。</p>
<p>看下 ondata() 方法里的几个核心实现：</p>
<ul>
<li><strong>dest.write(chunk)**：接收 chunk 写入数据，如果内部的缓冲小于创建流时配置的 highWaterMark，则返回 true，否则</strong>返回 false 时应该停止向流写入数据，直到 ‘drain’ 事件被触发**。</li>
<li>**src.pause()**：可读流会停止 data 事件，意味着此时暂停数据写入了。</li>
</ul>
<p>之所以<strong>调用 src.pause() 是为了防止读入数据过快来不及写入</strong>，什么时候知道来不及写入呢，要看 dest.write(chunk) 什么时候返回 false，是根据创建流时传的 highWaterMark 属性，默认为 16384 (16kb)，对象模式的流默认为 16。</p>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line">Readable.prototype.pipe = <span class="function"><span class="keyword">function</span>(<span class="params">dest, options</span>) </span>&#123;</span><br><span class="line">  <span class="keyword">const</span> src = <span class="built_in">this</span>;</span><br><span class="line">  src.on(<span class="string">&#x27;data&#x27;</span>, ondata);</span><br><span class="line">  <span class="function"><span class="keyword">function</span> <span class="title">ondata</span>(<span class="params">chunk</span>) </span>&#123;</span><br><span class="line">    <span class="keyword">const</span> ret = dest.write(chunk);</span><br><span class="line">    <span class="keyword">if</span> (ret === <span class="literal">false</span>) &#123;</span><br><span class="line">      ...</span><br><span class="line">      src.pause();</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line">  ...</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure>

<h4 id="15-2-2-3-订阅-drain-事件，继续流动数据"><a href="#15-2-2-3-订阅-drain-事件，继续流动数据" class="headerlink" title="15.2.2.3 订阅 drain 事件，继续流动数据"></a><strong>15.2.2.3 订阅 drain 事件，继续流动数据</strong></h4><p>上面提到在 data 事件里，如果调用 dest.write(chunk) 返回 false，就会调用 src.pause() 停止数据流动，什么时候再次开启呢？</p>
<p>如果说可以继续写入事件到流时会触发 drain 事件，也是在 dest.write(chunk) 等于 false 时，如果 ondrain 不存在则注册 drain 事件。</p>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br></pre></td><td class="code"><pre><span class="line">Readable.prototype.pipe = <span class="function"><span class="keyword">function</span>(<span class="params">dest, options</span>) </span>&#123;</span><br><span class="line">  <span class="keyword">const</span> src = <span class="built_in">this</span>;</span><br><span class="line">  src.on(<span class="string">&#x27;data&#x27;</span>, ondata);</span><br><span class="line">  <span class="function"><span class="keyword">function</span> <span class="title">ondata</span>(<span class="params">chunk</span>) </span>&#123;</span><br><span class="line">    <span class="keyword">const</span> ret = dest.write(chunk);</span><br><span class="line">    <span class="keyword">if</span> (ret === <span class="literal">false</span>) &#123;</span><br><span class="line">      ...</span><br><span class="line">      <span class="keyword">if</span> (!ondrain) &#123;</span><br><span class="line">        <span class="comment">// When the dest drains, it reduces the awaitDrain counter</span></span><br><span class="line">        <span class="comment">// on the source.  This would be more elegant with a .once()</span></span><br><span class="line">        <span class="comment">// handler in flow(), but adding and removing repeatedly is</span></span><br><span class="line">        <span class="comment">// too slow.</span></span><br><span class="line">        ondrain = pipeOnDrain(src);</span><br><span class="line">        dest.on(<span class="string">&#x27;drain&#x27;</span>, ondrain);</span><br><span class="line">      &#125;</span><br><span class="line">      src.pause();</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line">  ...</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 当可写入流 dest 耗尽时，它将会在可读流对象 source 上减少 awaitDrain 计数器</span></span><br><span class="line"><span class="comment">// 为了确保所有需要缓冲的写入都完成，即 state.awaitDrain === 0 和 src 可读流上的 data 事件存在，切换流到流动模式</span></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">pipeOnDrain</span>(<span class="params">src</span>) </span>&#123;</span><br><span class="line">  <span class="keyword">return</span> <span class="function"><span class="keyword">function</span> <span class="title">pipeOnDrainFunctionResult</span>(<span class="params"></span>) </span>&#123;</span><br><span class="line">    <span class="keyword">const</span> state = src._readableState;</span><br><span class="line">    debug(<span class="string">&#x27;pipeOnDrain&#x27;</span>, state.awaitDrain);</span><br><span class="line">    <span class="keyword">if</span> (state.awaitDrain)</span><br><span class="line">      state.awaitDrain--;</span><br><span class="line">    <span class="keyword">if</span> (state.awaitDrain === <span class="number">0</span> &amp;&amp; EE.listenerCount(src, <span class="string">&#x27;data&#x27;</span>)) &#123;</span><br><span class="line">      state.flowing = <span class="literal">true</span>;</span><br><span class="line">      flow(src);</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// stream.read() 从内部缓冲拉取并返回数据。如果没有可读的数据，则返回 null。在可读流上 src 还有一个 readable 属性，如果可以安全地调用 readable.read()，则为 true</span></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">flow</span>(<span class="params">stream</span>) </span>&#123;</span><br><span class="line">  <span class="keyword">const</span> state = stream._readableState;</span><br><span class="line">  debug(<span class="string">&#x27;flow&#x27;</span>, state.flowing);</span><br><span class="line">  <span class="keyword">while</span> (state.flowing &amp;&amp; stream.read() !== <span class="literal">null</span>);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<h4 id="15-2-2-4-触发-data-事件"><a href="#15-2-2-4-触发-data-事件" class="headerlink" title="15.2.2.4 触发 data 事件"></a><strong>15.2.2.4 触发 data 事件</strong></h4><p>调用 readable 的 resume() 方法，触发可读流的 ‘data’ 事件，进入流动模式。</p>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">Readable.prototype.pipe = <span class="function"><span class="keyword">function</span>(<span class="params">dest, options</span>) </span>&#123;</span><br><span class="line">  <span class="keyword">const</span> src = <span class="built_in">this</span>;</span><br><span class="line">  <span class="comment">// Start the flow if it hasn&#x27;t been started already.</span></span><br><span class="line">  <span class="keyword">if</span> (!state.flowing) &#123;</span><br><span class="line">    debug(<span class="string">&#x27;pipe resume&#x27;</span>);</span><br><span class="line">    src.resume();</span><br><span class="line">  &#125;</span><br><span class="line">  ...</span><br></pre></td></tr></table></figure>

<p>然后实例上的 resume（Readable 原型上定义的）会在调用 resume() 方法，在该方法内部又调用了 resume_()，最终执行了 stream.read(0) 读取了一次空数据（size 设置的为 0），将会触发实例上的 _read() 方法，之后会在触发 data 事件。</p>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">resume</span>(<span class="params">stream, state</span>) </span>&#123;</span><br><span class="line">  ...</span><br><span class="line">  process.nextTick(resume_, stream, state);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">resume_</span>(<span class="params">stream, state</span>) </span>&#123;</span><br><span class="line">  debug(<span class="string">&#x27;resume&#x27;</span>, state.reading);</span><br><span class="line">  <span class="keyword">if</span> (!state.reading) &#123;</span><br><span class="line">    stream.read(<span class="number">0</span>);</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  ...</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<h4 id="15-2-2-5-订阅-end-事件"><a href="#15-2-2-5-订阅-end-事件" class="headerlink" title="15.2.2.5 订阅 end 事件"></a><strong>15.2.2.5 订阅 end 事件</strong></h4><p>end 事件：当可读流中没有数据可供消费时触发，调用 onend 函数，执行 dest.end() 方法，表明已没有数据要被写入可写流，进行关闭（关闭可写流的 fd），之后再调用 stream.write() 会导致错误。</p>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line">Readable.prototype.pipe = <span class="function"><span class="keyword">function</span>(<span class="params">dest, options</span>) </span>&#123;</span><br><span class="line">  ...</span><br><span class="line">  <span class="keyword">const</span> doEnd = (!pipeOpts || pipeOpts.end !== <span class="literal">false</span>) &amp;&amp;</span><br><span class="line">              dest !== process.stdout &amp;&amp;</span><br><span class="line">              dest !== process.stderr;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">const</span> endFn = doEnd ? onend : unpipe;</span><br><span class="line">  <span class="keyword">if</span> (state.endEmitted)</span><br><span class="line">    process.nextTick(endFn);</span><br><span class="line">  <span class="keyword">else</span></span><br><span class="line">    src.once(<span class="string">&#x27;end&#x27;</span>, endFn);</span><br><span class="line"></span><br><span class="line">  dest.on(<span class="string">&#x27;unpipe&#x27;</span>, onunpipe);</span><br><span class="line">  ...</span><br><span class="line"></span><br><span class="line">  <span class="function"><span class="keyword">function</span> <span class="title">onend</span>(<span class="params"></span>) </span>&#123;</span><br><span class="line">    debug(<span class="string">&#x27;onend&#x27;</span>);</span><br><span class="line">    dest.end();</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<h4 id="15-2-2-6-触发-pipe-事件"><a href="#15-2-2-6-触发-pipe-事件" class="headerlink" title="15.2.2.6 触发 pipe 事件"></a><strong>15.2.2.6 触发 pipe 事件</strong></h4><p>在 pipe 方法里面最后还会触发一个 pipe 事件，传入可读流对象</p>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">Readable.prototype.pipe = <span class="function"><span class="keyword">function</span>(<span class="params">dest, options</span>) </span>&#123;</span><br><span class="line">  ...</span><br><span class="line">  <span class="keyword">const</span> source = <span class="built_in">this</span>;</span><br><span class="line">  dest.emit(<span class="string">&#x27;pipe&#x27;</span>, src);</span><br><span class="line">  ...</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure>

<p>在应用层使用的时候可以在可写流上订阅 pipe 事件，做一些判断，具体可参考官网给的这个示例 stream_event_pipe</p>
<h4 id="15-2-2-7-支持链式调用"><a href="#15-2-2-7-支持链式调用" class="headerlink" title="15.2.2.7 支持链式调用"></a><strong>15.2.2.7 支持链式调用</strong></h4><p>最后返回 dest，支持类似 unix 的用法：A.pipe(B).pipe(C)</p>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">Stream.prototype.pipe = <span class="function"><span class="keyword">function</span>(<span class="params">dest, options</span>) </span>&#123;</span><br><span class="line">  <span class="keyword">return</span> dest;</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure>

<h2 id="15-3-总结"><a href="#15-3-总结" class="headerlink" title="15.3 总结"></a><strong>15.3 总结</strong></h2><p>本文总体分为两部分：</p>
<ul>
<li>第一部分相对较基础，讲解了 Nodejs Stream 的 pipe 方法在 Koa2 中是怎么去应用的。</li>
<li>第二部分仍以 Nodejs Stream pipe 方法为题，查找它的实现，以及对源码的一个简单分析，其实 pipe 方法核心还是要去监听 data 事件，向可写流写入数据，如果内部缓冲大于创建流时配置的 highWaterMark，则要停止数据流动，直到 drain 事件触发或者结束，当然还要监听 end、error 等事件做一些处理。</li>
</ul>
<h2 id="15-4-Reference"><a href="#15-4-Reference" class="headerlink" title="15.4 Reference"></a><strong>15.4 Reference</strong></h2><ul>
<li>nodejs.cn/api/stream.html</li>
<li>cnodejs.org/topic/56ba030271204e03637a3870</li>
<li>github.com/nodejs/node/blob/master/lib/_stream_readable.js</li>
</ul>
<h1 id="16-守护进程"><a href="#16-守护进程" class="headerlink" title="16.守护进程"></a>16.守护进程</h1><h2 id="16-1-Nodejs编写守护进程"><a href="#16-1-Nodejs编写守护进程" class="headerlink" title="16.1 Nodejs编写守护进程"></a><strong>16.1 Nodejs编写守护进程</strong></h2><p>目前Nodejs编写一个守护进程非常简单，在6.3.1版本中已经存在非常方便的API，这些API可以帮助我们更方便的创建一个守护进程。本文仅在描述守护进程的创建方式，而不会对守护进程所要执行的任务做任何描述。</p>
<h2 id="16-2-守护进程的启动方式"><a href="#16-2-守护进程的启动方式" class="headerlink" title="16.2 守护进程的启动方式"></a>16.2 守护进程的启动方式</h2><p>如果不在Nodejs环境中，我们如何创建守护进程？过程如下：</p>
<ol>
<li>创建一个进程A。</li>
<li>在进程A中创建进程B，我们可以使用fork方式，或者其他方法。</li>
<li>对进程B执行 <code>setsid</code> 方法。</li>
<li>进程A退出，进程B由init进程接管。此时进程B为守护进程。</li>
</ol>
<h2 id="16-3-setsid详解"><a href="#16-3-setsid详解" class="headerlink" title="16.3 setsid详解"></a>16.3 setsid详解</h2><p><code>setsid</code> 主要完成三件事：</p>
<ol>
<li>该进程变成一个新会话的会话领导。</li>
<li>该进程变成一个新进程组的组长。</li>
<li>该进程没有控制终端。</li>
</ol>
<p>然而，Nodejs中并没有对 <code>setsid</code> 方法的直接封装，翻阅文档发现有一个地方是可以调用该方法的。</p>
<h2 id="16-4-Nodejs中启动子进程方法"><a href="#16-4-Nodejs中启动子进程方法" class="headerlink" title="16.4 Nodejs中启动子进程方法"></a>16.4 Nodejs中启动子进程方法</h2><p>借助 <code>clild_process</code> 中的 <code>spawn</code> 即可创建子进程，方法如下：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">ar spawn &#x3D; require(&#39;child_process&#39;).spawn;</span><br><span class="line">var process &#x3D; require(&#39;process&#39;);</span><br><span class="line"></span><br><span class="line">var p &#x3D; spawn(&#39;node&#39;,[&#39;b.js&#39;]);</span><br><span class="line">console.log(process.pid, p.pid);</span><br></pre></td></tr></table></figure>

<p>注意，这里只打印当前进程的PID和子进程的PID，同时为了观察效果，我并没有将父进程退出。</p>
<p><code>b.js</code> 中代码很简单，打开一个资源，并不停的写入数据。</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line">var fs &#x3D; require(&#39;fs&#39;);</span><br><span class="line">var process &#x3D; require(&#39;process&#39;);</span><br><span class="line"></span><br><span class="line">fs.open(&quot;&#x2F;Users&#x2F;mebius&#x2F;Desktop&#x2F;log.txt&quot;,&#39;w&#39;,function(err, fd)&#123;</span><br><span class="line">	console.log(fd);</span><br><span class="line">	while(true)</span><br><span class="line">	&#123;</span><br><span class="line">		fs.write(fd,process.pid+&quot;\n&quot;,function()&#123;&#125;);</span><br><span class="line">	&#125;</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure>

<p>运行后的效果如图：</p>
<p><img data-src="https://user-gold-cdn.xitu.io/2016/11/29/7a21cc1b1975ad2128ed094f5e8effc6.png?imageView2/0/w/1280/h/960/format/webp/ignore-error/1" alt="img"></p>
<p>我们来看以下 <code>top</code> 命令下的进程情况。</p>
<p><img data-src="https://user-gold-cdn.xitu.io/2016/11/29/52bf06943ce2b7f2acfc9095bb202ece.png?imageView2/0/w/1280/h/960/format/webp/ignore-error/1" alt="img"></p>
<p>看一看到，此时父进程PID为17055，子进程的PPID为17055，PID为17056.</p>
<h2 id="16-5-Nodejs中setsid的调用"><a href="#16-5-Nodejs中setsid的调用" class="headerlink" title="16.5 Nodejs中setsid的调用"></a>16.5 Nodejs中setsid的调用</h2><p>到此为止，守护进程已经完成一半，下面要调用setsid方法，并且退出父进程。</p>
<p>代码修改如下：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">var spawn &#x3D; require(&#39;child_process&#39;).spawn;</span><br><span class="line">var process &#x3D; require(&#39;process&#39;);</span><br><span class="line"></span><br><span class="line">var p &#x3D; spawn(&#39;node&#39;,[&#39;b.js&#39;],&#123;</span><br><span class="line">        detached : true</span><br><span class="line">    &#125;);</span><br><span class="line">console.log(process.pid, p.pid);</span><br><span class="line">process.exit(0);</span><br></pre></td></tr></table></figure>

<p>在 <code>spawn</code> 的第三个参数中，可以设置 <code>detached</code> 属性，如果该属性为true，则会调用 <code>setsid</code> 方法。这样就满足我们对守护进程的要求。</p>
<p>在此运行命令。</p>
<p><img data-src="https://user-gold-cdn.xitu.io/2016/11/29/04ffe72d6460092a8b5796f6f6099734.png?imageView2/0/w/1280/h/960/format/webp/ignore-error/1" alt="img"></p>
<p>查看 <code>top</code> 命令</p>
<p><img data-src="https://user-gold-cdn.xitu.io/2016/11/29/c05d3cae380bb9f107f09793c8c67923.png?imageView2/0/w/1280/h/960/format/webp/ignore-error/1" alt="img"></p>
<p>可以看到，当前仅存在一个PID为17062的进程，这个进程就是我们要的守护进程。</p>
<blockquote>
<p>由于每次运行PID都不同，所以此次子进程的PID于第一次不同。</p>
</blockquote>
<h2 id="总结-2"><a href="#总结-2" class="headerlink" title="总结"></a>总结</h2><p>守护进程最重要的是稳定，如果守护进程挂掉，那么其管理的子进程都将变为孤儿进程，同时被init进程接管，这是我们不愿意看到的。于此同时，守护进程对于子进程的管理也是有非常多的发挥余地的，例如PM2中，将一个进程同时启动4次，达到CPU多核使用的目的（很有可能你的进程在同一核中运行），进程挂掉后自动重启等等，这些事情等着我们去造轮子。</p>
<p>总体来说，Nodejs启动守护进程方式比较简单，默认所暴露的API也屏蔽了很多系统级别API，使得大家使用上更加方便，但没有接触过Linux的人在理解上有一些复杂。推荐大家学习Nodejs的同时，多学习Linux系统调用的和系统内核的一些东西。</p>
<h1 id="17-Nodejs进程间通信"><a href="#17-Nodejs进程间通信" class="headerlink" title="17.Nodejs进程间通信"></a>17.Nodejs进程间通信</h1><h2 id="17-1-场景"><a href="#17-1-场景" class="headerlink" title="17.1 场景"></a>17.1 场景</h2><p>Node运行在单线程下，但这并不意味着无法利用多核/多机下多进程的优势</p>
<p>事实上，Node最初从设计上就考虑了分布式网络场景：</p>
<blockquote>
<p>Node is a single-threaded, single-process system which enforces shared-nothing design with OS process boundaries. It has rather good libraries for networking. I believe this to be a basis for designing very large distributed programs. The “nodes” need to be organized: given a communication protocol, told how to connect to each other. In the next couple months we are working on libraries for Node that allow these networks.</p>
</blockquote>
<p>P.S.关于Node之所以叫Node，见<a target="_blank" rel="noopener" href="https://stackoverflow.com/questions/5621812/why-is-node-js-named-node-js">Why is Node.js named Node.js?</a></p>
<h2 id="17-2-创建进程"><a href="#17-2-创建进程" class="headerlink" title="17.2 创建进程"></a>17.2 创建进程</h2><p>通信方式与进程产生方式有关，而Node有4种创建进程的方式：<code>spawn()</code>，<code>exec()</code>，<code>execFile()</code>和<code>fork()</code></p>
<h3 id="spawn"><a href="#spawn" class="headerlink" title="spawn"></a>spawn</h3><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">const &#123; spawn &#125; &#x3D; require(&#39;child_process&#39;);</span><br><span class="line">const child &#x3D; spawn(&#39;pwd&#39;);</span><br><span class="line">&#x2F;&#x2F; 带参数的形式</span><br><span class="line">&#x2F;&#x2F; const child &#x3D; spawn(&#39;find&#39;, [&#39;.&#39;, &#39;-type&#39;, &#39;f&#39;]);</span><br></pre></td></tr></table></figure>

<p><code>spawn()</code>返回<code>ChildProcess</code>实例，<code>ChildProcess</code>同样基于事件机制（EventEmitter API），提供了一些事件：</p>
<ul>
<li><code>exit</code>：子进程退出时触发，可以得知进程退出状态（<code>code</code>和<code>signal</code>）</li>
<li><code>disconnect</code>：父进程调用<code>child.disconnect()</code>时触发</li>
<li><code>error</code>：子进程创建失败，或被<code>kill</code>时触发</li>
<li><code>close</code>：子进程的<code>stdio</code>流（标准输入输出流）关闭时触发</li>
<li><code>message</code>：子进程通过<code>process.send()</code>发送消息时触发，父子进程之间可以通过这种<em>内置的消息机制通信</em></li>
</ul>
<p>可以通过<code>child.stdin</code>，<code>child.stdout</code>和<code>child.stderr</code>访问子进程的<code>stdio</code>流，这些流被关闭的时，子进程会触发<code>close</code>事件</p>
<p>P.S.<code>close</code>与<code>exit</code>的区别主要体现在多进程共享同一<code>stdio</code>流的场景，某个进程退出了并不意味着<code>stdio</code>流被关闭了</p>
<p>在子进程中，<code>stdout/stderr</code>具有Readable特性，而<code>stdin</code>具有Writable特性，<em>与主进程的情况正好相反</em>：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">child.stdout.on(&#39;data&#39;, (data) &#x3D;&gt; &#123;</span><br><span class="line">  console.log(&#96;child stdout:\n$&#123;data&#125;&#96;);</span><br><span class="line">&#125;);</span><br><span class="line"></span><br><span class="line">child.stderr.on(&#39;data&#39;, (data) &#x3D;&gt; &#123;</span><br><span class="line">  console.error(&#96;child stderr:\n$&#123;data&#125;&#96;);</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure>

<p>利用进程<code>stdio</code>流的管道特性，就可以完成更复杂的事情，例如：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line">const &#123; spawn &#125; &#x3D; require(&#39;child_process&#39;);</span><br><span class="line"></span><br><span class="line">const find &#x3D; spawn(&#39;find&#39;, [&#39;.&#39;, &#39;-type&#39;, &#39;f&#39;]);</span><br><span class="line">const wc &#x3D; spawn(&#39;wc&#39;, [&#39;-l&#39;]);</span><br><span class="line"></span><br><span class="line">find.stdout.pipe(wc.stdin);</span><br><span class="line"></span><br><span class="line">wc.stdout.on(&#39;data&#39;, (data) &#x3D;&gt; &#123;</span><br><span class="line">  console.log(&#96;Number of files $&#123;data&#125;&#96;);</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure>

<p>作用等价于<code>find . -type f | wc -l</code>，递归统计当前目录文件数量</p>
<h4 id="IPC选项"><a href="#IPC选项" class="headerlink" title="IPC选项"></a>IPC选项</h4><p>另外，通过<code>spawn()</code>方法的<code>stdio</code>选项可以建立IPC机制：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line">const &#123; spawn &#125; &#x3D; require(&#39;child_process&#39;);</span><br><span class="line"></span><br><span class="line">const child &#x3D; spawn(&#39;node&#39;, [&#39;.&#x2F;ipc-child.js&#39;], &#123; stdio: [null, null, null, &#39;ipc&#39;] &#125;);</span><br><span class="line">child.on(&#39;message&#39;, (m) &#x3D;&gt; &#123;</span><br><span class="line">  console.log(m);</span><br><span class="line">&#125;);</span><br><span class="line">child.send(&#39;Here Here&#39;);</span><br><span class="line"></span><br><span class="line">&#x2F;&#x2F; .&#x2F;ipc-child.js</span><br><span class="line">process.on(&#39;message&#39;, (m) &#x3D;&gt; &#123;</span><br><span class="line">  process.send(&#96;&lt; $&#123;m&#125;&#96;);</span><br><span class="line">  process.send(&#39;&gt; 不要回答x3&#39;);</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure>

<p>关于<code>spawn()</code>的IPC选项的详细信息，请查看<a target="_blank" rel="noopener" href="https://nodejs.org/dist/latest-v8.x/docs/api/child_process.html#child_process_options_stdio">options.stdio</a></p>
<h3 id="exec"><a href="#exec" class="headerlink" title="exec"></a>exec</h3><p><code>spawn()</code>方法默认不会创建shell去执行传入的命令（所以<em>性能上稍微好一点</em>），而<code>exec()</code>方法会创建一个shell。另外，<code>exec()</code>不是基于stream的，而是把传入命令的执行结果暂存到buffer中，再整个传递给回调函数</p>
<p><code>exec()</code>方法的特点是<em>完全支持shell语法</em>，可以直接传入任意shell脚本，例如：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line">const &#123; exec &#125; &#x3D; require(&#39;child_process&#39;);</span><br><span class="line"></span><br><span class="line">exec(&#39;find . -type f | wc -l&#39;, (err, stdout, stderr) &#x3D;&gt; &#123;</span><br><span class="line">  if (err) &#123;</span><br><span class="line">    console.error(&#96;exec error: $&#123;err&#125;&#96;);</span><br><span class="line">    return;</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  console.log(&#96;Number of files $&#123;stdout&#125;&#96;);</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure>

<p>但<code>exec()</code>方法也因此存在<a target="_blank" rel="noopener" href="https://blog.liftsecurity.io/2014/08/19/Avoid-Command-Injection-Node.js/">命令注入</a>的安全风险，在含有用户输入等动态内容的场景要特别注意。所以，<code>exec()</code>方法的适用场景是：希望直接使用shell语法，并且预期输出数据量不大（不存在内存压力）</p>
<p>那么，有没有既支持shell语法，还具有stream IO优势的方式？</p>
<p>有。<em>两全其美的方式</em>如下：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">const &#123; spawn &#125; &#x3D; require(&#39;child_process&#39;);</span><br><span class="line">const child &#x3D; spawn(&#39;find . -type f | wc -l&#39;, &#123;</span><br><span class="line">  shell: true</span><br><span class="line">&#125;);</span><br><span class="line">child.stdout.pipe(process.stdout);</span><br></pre></td></tr></table></figure>

<p>开启<code>spawn()</code>的<code>shell</code>选项，并通过<code>pipe()</code>方法把子进程的标准输出简单地接到当前进程的标准输入上，以便看到命令执行结果。实际上还有更容易的方式：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">const &#123; spawn &#125; &#x3D; require(&#39;child_process&#39;);</span><br><span class="line">process.stdout.on(&#39;data&#39;, (data) &#x3D;&gt; &#123;</span><br><span class="line">  console.log(data);</span><br><span class="line">&#125;);</span><br><span class="line">const child &#x3D; spawn(&#39;find . -type f | wc -l&#39;, &#123;</span><br><span class="line">  shell: true,</span><br><span class="line">  stdio: &#39;inherit&#39;</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure>

<p><code>stdio: &#39;inherit&#39;</code>允许子进程继承当前进程的标准输入输出（共享<code>stdin</code>，<code>stdout</code>和<code>stderr</code>），所以上例能够通过监听当前进程<code>process.stdout</code>的<code>data</code>事件拿到子进程的输出结果</p>
<p>另外，除了<code>stdio</code>和<code>shell</code>选项，<code>spawn()</code>还支持一些其它选项，如：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line">const child &#x3D; spawn(&#39;find . -type f | wc -l&#39;, &#123;</span><br><span class="line">  stdio: &#39;inherit&#39;,</span><br><span class="line">  shell: true,</span><br><span class="line">  &#x2F;&#x2F; 修改环境变量，默认process.env</span><br><span class="line">  env: &#123; HOME: &#39;&#x2F;tmp&#x2F;xxx&#39; &#125;,</span><br><span class="line">  &#x2F;&#x2F; 改变当前工作目录</span><br><span class="line">  cwd: &#39;&#x2F;tmp&#39;,</span><br><span class="line">  &#x2F;&#x2F; 作为独立进程存在</span><br><span class="line">  detached: true</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure>

<p><em>注意</em>，<code>env</code>选项除了以环境变量形式向子进程传递数据外，还可以用来实现沙箱式的环境变量隔离，默认把<code>process.env</code>作为子进程的环境变量集，子进程与当前进程一样能够访问所有环境变量，如果像上例中指定自定义对象作为子进程的环境变量集，子进程就无法访问其它环境变量</p>
<p>所以，想要增/删环境变量的话，需要这样做：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">var spawn_env &#x3D; JSON.parse(JSON.stringify(process.env));</span><br><span class="line"></span><br><span class="line">&#x2F;&#x2F; remove those env vars</span><br><span class="line">delete spawn_env.ATOM_SHELL_INTERNAL_RUN_AS_NODE;</span><br><span class="line">delete spawn_env.ELECTRON_RUN_AS_NODE;</span><br><span class="line"></span><br><span class="line">var sp &#x3D; spawn(command, [&#39;.&#39;], &#123;cwd: cwd, env: spawn_env&#125;);</span><br></pre></td></tr></table></figure>

<p><code>detached</code>选项更有意思：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">const &#123; spawn &#125; &#x3D; require(&#39;child_process&#39;);</span><br><span class="line"></span><br><span class="line">const child &#x3D; spawn(&#39;node&#39;, [&#39;stuff.js&#39;], &#123;</span><br><span class="line">  detached: true,</span><br><span class="line">  stdio: &#39;ignore&#39;</span><br><span class="line">&#125;);</span><br><span class="line"></span><br><span class="line">child.unref();</span><br></pre></td></tr></table></figure>

<p>以这种方式创建的独立进程行为取决于操作系统，Windows上detached子进程将拥有自己的console窗口，而Linux上该进程会<em>创建新的process group</em>（这个特性可以用来管理子进程族，实现类似于<a target="_blank" rel="noopener" href="http://www.ayqy.net/blog/nodejs%E8%BF%9B%E7%A8%8B%E9%97%B4%E9%80%9A%E4%BF%A1/npmjs.com/package/tree-kill">tree-kill</a>的特性）</p>
<p><code>unref()</code>方法用来断绝关系，这样“父”进程可以独立退出（不会导致子进程跟着退出），但要注意这时子进程的<code>stdio</code>也应该独立于“父”进程，否则“父”进程退出后子进程仍会受到影响</p>
<h3 id="execFile"><a href="#execFile" class="headerlink" title="execFile"></a>execFile</h3><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">const &#123; execFile &#125; &#x3D; require(&#39;child_process&#39;);</span><br><span class="line">const child &#x3D; execFile(&#39;node&#39;, [&#39;--version&#39;], (error, stdout, stderr) &#x3D;&gt; &#123;</span><br><span class="line">  if (error) &#123;</span><br><span class="line">    throw error;</span><br><span class="line">  &#125;</span><br><span class="line">  console.log(stdout);</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure>

<p>与<code>exec()</code>方法类似，但不通过shell来执行（所以性能稍好一点），所以要求传入<em>可执行文件</em>。Windows下某些文件无法直接执行，比如<code>.bat</code>和<code>.cmd</code>，这些文件就不能用<code>execFile()</code>来执行，只能借助<code>exec()</code>或开启了<code>shell</code>选项的<code>spawn()</code></p>
<p>P.S.与<code>exec()</code>一样也<em>不是基于stream的</em>，同样存在输出数据量风险</p>
<h4 id="xxxSync"><a href="#xxxSync" class="headerlink" title="xxxSync"></a>xxxSync</h4><p><code>spawn</code>，<code>exec</code>和<code>execFile</code>都有对应的同步阻塞版本，一直等到子进程退出</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">const &#123; </span><br><span class="line">  spawnSync, </span><br><span class="line">  execSync, </span><br><span class="line">  execFileSync,</span><br><span class="line">&#125; &#x3D; require(&#39;child_process&#39;);</span><br></pre></td></tr></table></figure>

<p>同步方法用来简化脚本任务，比如启动流程，其它时候应该避免使用这些方法</p>
<h3 id="fork"><a href="#fork" class="headerlink" title="fork"></a>fork</h3><p><code>fork()</code>是<code>spawn()</code>的变体，用来创建Node进程，最大的特点是父子进程自带通信机制（IPC管道）：</p>
<blockquote>
<p>The child_process.fork() method is a special case of child_process.spawn() used specifically to spawn new Node.js processes. Like child_process.spawn(), a ChildProcess object is returned. The returned ChildProcess will have an additional communication channel built-in that allows messages to be passed back and forth between the parent and child. See subprocess.send() for details.</p>
</blockquote>
<p>例如：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">var n &#x3D; child_process.fork(&#39;.&#x2F;child.js&#39;);</span><br><span class="line">n.on(&#39;message&#39;, function(m) &#123;</span><br><span class="line">  console.log(&#39;PARENT got message:&#39;, m);</span><br><span class="line">&#125;);</span><br><span class="line">n.send(&#123; hello: &#39;world&#39; &#125;);</span><br><span class="line"></span><br><span class="line">&#x2F;&#x2F; .&#x2F;child.js</span><br><span class="line">process.on(&#39;message&#39;, function(m) &#123;</span><br><span class="line">  console.log(&#39;CHILD got message:&#39;, m);</span><br><span class="line">&#125;);</span><br><span class="line">process.send(&#123; foo: &#39;bar&#39; &#125;);</span><br></pre></td></tr></table></figure>

<p>因为<code>fork()</code>自带通信机制的优势，尤其适合用来拆分耗时逻辑，例如：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line">const http &#x3D; require(&#39;http&#39;);</span><br><span class="line">const longComputation &#x3D; () &#x3D;&gt; &#123;</span><br><span class="line">  let sum &#x3D; 0;</span><br><span class="line">  for (let i &#x3D; 0; i &lt; 1e9; i++) &#123;</span><br><span class="line">    sum +&#x3D; i;</span><br><span class="line">  &#125;;</span><br><span class="line">  return sum;</span><br><span class="line">&#125;;</span><br><span class="line">const server &#x3D; http.createServer();</span><br><span class="line">server.on(&#39;request&#39;, (req, res) &#x3D;&gt; &#123;</span><br><span class="line">  if (req.url &#x3D;&#x3D;&#x3D; &#39;&#x2F;compute&#39;) &#123;</span><br><span class="line">    const sum &#x3D; longComputation();</span><br><span class="line">    return res.end(&#96;Sum is $&#123;sum&#125;&#96;);</span><br><span class="line">  &#125; else &#123;</span><br><span class="line">    res.end(&#39;Ok&#39;)</span><br><span class="line">  &#125;</span><br><span class="line">&#125;);</span><br><span class="line"></span><br><span class="line">server.listen(3000);</span><br></pre></td></tr></table></figure>

<p>这样做的致命问题是一旦有人访问<code>/compute</code>，后续请求都无法及时处理，因为事件循环还被<code>longComputation</code>阻塞着，直到耗时计算结束才能恢复服务能力</p>
<p>为了避免耗时操作阻塞主进程的事件循环，可以把<code>longComputation()</code>拆分到子进程中：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line">&#x2F;&#x2F; compute.js</span><br><span class="line">const longComputation &#x3D; () &#x3D;&gt; &#123;</span><br><span class="line">  let sum &#x3D; 0;</span><br><span class="line">  for (let i &#x3D; 0; i &lt; 1e9; i++) &#123;</span><br><span class="line">    sum +&#x3D; i;</span><br><span class="line">  &#125;;</span><br><span class="line">  return sum;</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line">&#x2F;&#x2F; 开关，收到消息才开始做</span><br><span class="line">process.on(&#39;message&#39;, (msg) &#x3D;&gt; &#123;</span><br><span class="line">  const sum &#x3D; longComputation();</span><br><span class="line">  process.send(sum);</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure>

<p>主进程开启子进程执行<code>longComputation</code>：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line">const http &#x3D; require(&#39;http&#39;);</span><br><span class="line">const &#123; fork &#125; &#x3D; require(&#39;child_process&#39;);</span><br><span class="line"></span><br><span class="line">const server &#x3D; http.createServer();</span><br><span class="line"></span><br><span class="line">server.on(&#39;request&#39;, (req, res) &#x3D;&gt; &#123;</span><br><span class="line">  if (req.url &#x3D;&#x3D;&#x3D; &#39;&#x2F;compute&#39;) &#123;</span><br><span class="line">    const compute &#x3D; fork(&#39;compute.js&#39;);</span><br><span class="line">    compute.send(&#39;start&#39;);</span><br><span class="line">    compute.on(&#39;message&#39;, sum &#x3D;&gt; &#123;</span><br><span class="line">      res.end(&#96;Sum is $&#123;sum&#125;&#96;);</span><br><span class="line">    &#125;);</span><br><span class="line">  &#125; else &#123;</span><br><span class="line">    res.end(&#39;Ok&#39;)</span><br><span class="line">  &#125;</span><br><span class="line">&#125;);</span><br><span class="line"></span><br><span class="line">server.listen(3000);</span><br></pre></td></tr></table></figure>

<p>主进程的事件循环不会再被耗时计算阻塞，但进程数量还需要进一步限制，否则资源被进程消耗殆尽时服务能力仍会受到影响</p>
<p>P.S.实际上，<code>cluster</code>模块就是对多进程服务能力的封装，<em>思路与这个简单示例类似</em></p>
<h2 id="17-3-通信方式"><a href="#17-3-通信方式" class="headerlink" title="17.3 通信方式"></a>17.3 通信方式</h2><h3 id="17-3-1-通过stdin-stdout传递json"><a href="#17-3-1-通过stdin-stdout传递json" class="headerlink" title="17.3.1 通过stdin/stdout传递json"></a>17.3.1 通过stdin/stdout传递json</h3><blockquote>
<p>stdin/stdout and a JSON payload</p>
</blockquote>
<p>最直接的通信方式，拿到子进程的handle后，可以访问其<code>stdio</code>流，然后约定一种<code>message</code>格式开始愉快地通信：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line">const &#123; spawn &#125; &#x3D; require(&#39;child_process&#39;);</span><br><span class="line"></span><br><span class="line">child &#x3D; spawn(&#39;node&#39;, [&#39;.&#x2F;stdio-child.js&#39;]);</span><br><span class="line">child.stdout.setEncoding(&#39;utf8&#39;);</span><br><span class="line">&#x2F;&#x2F; 父进程-发</span><br><span class="line">child.stdin.write(JSON.stringify(&#123;</span><br><span class="line">  type: &#39;handshake&#39;,</span><br><span class="line">  payload: &#39;你好吖&#39;</span><br><span class="line">&#125;));</span><br><span class="line">&#x2F;&#x2F; 父进程-收</span><br><span class="line">child.stdout.on(&#39;data&#39;, function (chunk) &#123;</span><br><span class="line">  let data &#x3D; chunk.toString();</span><br><span class="line">  let message &#x3D; JSON.parse(data);</span><br><span class="line">  console.log(&#96;$&#123;message.type&#125; $&#123;message.payload&#125;&#96;);</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure>

<p>子进程与之类似：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line">&#x2F;&#x2F; .&#x2F;stdio-child.js</span><br><span class="line">&#x2F;&#x2F; 子进程-收</span><br><span class="line">process.stdin.on(&#39;data&#39;, (chunk) &#x3D;&gt; &#123;</span><br><span class="line">  let data &#x3D; chunk.toString();</span><br><span class="line">  let message &#x3D; JSON.parse(data);</span><br><span class="line">  switch (message.type) &#123;</span><br><span class="line">    case &#39;handshake&#39;:</span><br><span class="line">      &#x2F;&#x2F; 子进程-发</span><br><span class="line">      process.stdout.write(JSON.stringify(&#123;</span><br><span class="line">        type: &#39;message&#39;,</span><br><span class="line">        payload: message.payload + &#39; : hoho&#39;</span><br><span class="line">      &#125;));</span><br><span class="line">      break;</span><br><span class="line">    default:</span><br><span class="line">      break;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure>

<p>P.S.VS Code进程间通信就采用了这种方式，具体见<a target="_blank" rel="noopener" href="https://github.com/Microsoft/vscode/issues/3011#issuecomment-196305696">access electron API from vscode extension</a></p>
<p>明显的<em>限制</em>是需要拿到“子”进程的handle，两个完全独立的进程之间无法通过这种方式来通信（比如跨应用，甚至跨机器的场景）</p>
<p>P.S.关于stream及pipe的详细信息，请查看<a target="_blank" rel="noopener" href="http://www.ayqy.net/blog/node-stream/">Node中的流</a></p>
<h3 id="17-3-2-原生IPC支持"><a href="#17-3-2-原生IPC支持" class="headerlink" title="17.3.2 原生IPC支持"></a>17.3.2 原生IPC支持</h3><p>如<code>spawn()</code>及<code>fork()</code>的例子，进程之间可以借助内置的IPC机制通信</p>
<p>父进程：</p>
<ul>
<li><code>process.on(&#39;message&#39;)</code>收</li>
<li><code>child.send()</code>发</li>
</ul>
<p>子进程：</p>
<ul>
<li><code>process.on(&#39;message&#39;)</code>收</li>
<li><code>process.send()</code>发</li>
</ul>
<p>限制同上，同样要有一方能够拿到另一方的handle才行</p>
<h3 id="17-3-3-sockets"><a href="#17-3-3-sockets" class="headerlink" title="17.3.3 sockets"></a>17.3.3 sockets</h3><p>借助网络来完成进程间通信，<em>不仅能跨进程，还能跨机器</em></p>
<p><a target="_blank" rel="noopener" href="https://www.npmjs.com/package/node-ipc">node-ipc</a>就采用这种方案，例如：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br></pre></td><td class="code"><pre><span class="line">&#x2F;&#x2F; server</span><br><span class="line">const ipc&#x3D;require(&#39;..&#x2F;..&#x2F;..&#x2F;node-ipc&#39;);</span><br><span class="line"></span><br><span class="line">ipc.config.id &#x3D; &#39;world&#39;;</span><br><span class="line">ipc.config.retry&#x3D; 1500;</span><br><span class="line">ipc.config.maxConnections&#x3D;1;</span><br><span class="line"></span><br><span class="line">ipc.serveNet(</span><br><span class="line">    function()&#123;</span><br><span class="line">        ipc.server.on(</span><br><span class="line">            &#39;message&#39;,</span><br><span class="line">            function(data,socket)&#123;</span><br><span class="line">                ipc.log(&#39;got a message : &#39;, data);</span><br><span class="line">                ipc.server.emit(</span><br><span class="line">                    socket,</span><br><span class="line">                    &#39;message&#39;,</span><br><span class="line">                    data+&#39; world!&#39;</span><br><span class="line">                );</span><br><span class="line">            &#125;</span><br><span class="line">        );</span><br><span class="line"></span><br><span class="line">        ipc.server.on(</span><br><span class="line">            &#39;socket.disconnected&#39;,</span><br><span class="line">            function(data,socket)&#123;</span><br><span class="line">                console.log(&#39;DISCONNECTED\n\n&#39;,arguments);</span><br><span class="line">            &#125;</span><br><span class="line">        );</span><br><span class="line">    &#125;</span><br><span class="line">);</span><br><span class="line">ipc.server.on(</span><br><span class="line">    &#39;error&#39;,</span><br><span class="line">    function(err)&#123;</span><br><span class="line">        ipc.log(&#39;Got an ERROR!&#39;,err);</span><br><span class="line">    &#125;</span><br><span class="line">);</span><br><span class="line">ipc.server.start();</span><br><span class="line"></span><br><span class="line">&#x2F;&#x2F; client</span><br><span class="line">const ipc&#x3D;require(&#39;node-ipc&#39;);</span><br><span class="line"></span><br><span class="line">ipc.config.id &#x3D; &#39;hello&#39;;</span><br><span class="line">ipc.config.retry&#x3D; 1500;</span><br><span class="line"></span><br><span class="line">ipc.connectToNet(</span><br><span class="line">    &#39;world&#39;,</span><br><span class="line">    function()&#123;</span><br><span class="line">        ipc.of.world.on(</span><br><span class="line">            &#39;connect&#39;,</span><br><span class="line">            function()&#123;</span><br><span class="line">                ipc.log(&#39;## connected to world ##&#39;, ipc.config.delay);</span><br><span class="line">                ipc.of.world.emit(</span><br><span class="line">                    &#39;message&#39;,</span><br><span class="line">                    &#39;hello&#39;</span><br><span class="line">                );</span><br><span class="line">            &#125;</span><br><span class="line">        );</span><br><span class="line">        ipc.of.world.on(</span><br><span class="line">            &#39;disconnect&#39;,</span><br><span class="line">            function()&#123;</span><br><span class="line">                ipc.log(&#39;disconnected from world&#39;);</span><br><span class="line">            &#125;</span><br><span class="line">        );</span><br><span class="line">        ipc.of.world.on(</span><br><span class="line">            &#39;message&#39;,</span><br><span class="line">            function(data)&#123;</span><br><span class="line">                ipc.log(&#39;got a message from world : &#39;, data);</span><br><span class="line">            &#125;</span><br><span class="line">        );</span><br><span class="line">    &#125;</span><br><span class="line">);</span><br></pre></td></tr></table></figure>

<p>P.S.更多示例见<a target="_blank" rel="noopener" href="https://github.com/RIAEvangelist/node-ipc/tree/master/example">RIAEvangelist/node-ipc</a></p>
<p>当然，单机场景下通过网络来完成进程间通信有些浪费性能，但网络通信的<em>优势</em>在于跨环境的兼容性与更进一步的RPC场景</p>
<h3 id="17-3-4-message-queue"><a href="#17-3-4-message-queue" class="headerlink" title="17.3.4 message queue"></a>17.3.4 message queue</h3><p>父子进程都通过外部消息机制来通信，跨进程的能力取决于MQ支持</p>
<p>即进程间不直接通信，而是通过中间层（MQ），<em>加一个控制层</em>就能获得更多灵活性和优势：</p>
<ul>
<li>稳定性：消息机制提供了强大的稳定性保证，比如确认送达（消息回执ACK），失败重发/防止多发等等</li>
<li>优先级控制：允许调整消息响应次序</li>
<li>离线能力：消息可以被缓存</li>
<li>事务性消息处理：把关联消息组合成事务，保证其送达顺序及完整性</li>
</ul>
<p>P.S.不好实现？包一层能解决吗，不行就包两层……</p>
<p>比较受欢迎的有<a target="_blank" rel="noopener" href="https://github.com/smrchy/rsmq">smrchy/rsmq</a>，例如：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><span class="line">&#x2F;&#x2F; init</span><br><span class="line">RedisSMQ &#x3D; require(&quot;rsmq&quot;);</span><br><span class="line">rsmq &#x3D; new RedisSMQ( &#123;host: &quot;127.0.0.1&quot;, port: 6379, ns: &quot;rsmq&quot;&#125; );</span><br><span class="line">&#x2F;&#x2F; create queue</span><br><span class="line">rsmq.createQueue(&#123;qname:&quot;myqueue&quot;&#125;, function (err, resp) &#123;</span><br><span class="line">    if (resp&#x3D;&#x3D;&#x3D;1) &#123;</span><br><span class="line">      console.log(&quot;queue created&quot;)</span><br><span class="line">    &#125;</span><br><span class="line">&#125;);</span><br><span class="line">&#x2F;&#x2F; send message</span><br><span class="line">rsmq.sendMessage(&#123;qname:&quot;myqueue&quot;, message:&quot;Hello World&quot;&#125;, function (err, resp) &#123;</span><br><span class="line">  if (resp) &#123;</span><br><span class="line">    console.log(&quot;Message sent. ID:&quot;, resp);</span><br><span class="line">  &#125;</span><br><span class="line">&#125;);</span><br><span class="line">&#x2F;&#x2F; receive message</span><br><span class="line">rsmq.receiveMessage(&#123;qname:&quot;myqueue&quot;&#125;, function (err, resp) &#123;</span><br><span class="line">  if (resp.id) &#123;</span><br><span class="line">    console.log(&quot;Message received.&quot;, resp)  </span><br><span class="line">  &#125;</span><br><span class="line">  else &#123;</span><br><span class="line">    console.log(&quot;No messages for me...&quot;)</span><br><span class="line">  &#125;</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure>

<p>会起一个Redis server，基本原理如下：</p>
<blockquote>
<p>Using a shared Redis server multiple Node.js processes can send / receive messages.</p>
</blockquote>
<p>消息的收/发/缓存/持久化依靠Redis提供的能力，在此基础上实现完整的队列机制</p>
<h3 id="17-3-5-Redis"><a href="#17-3-5-Redis" class="headerlink" title="17.3.5 Redis"></a>17.3.5 Redis</h3><p>基本思路与message queue类似：</p>
<blockquote>
<p>Use Redis as a message bus/broker.</p>
</blockquote>
<p>Redis自带<a target="_blank" rel="noopener" href="https://redis.io/topics/pubsub">Pub/Sub机制</a>（即发布-订阅模式），适用于简单的通信场景，比如一对一或一对多并且<em>不关注消息可靠性</em>的场景</p>
<p>另外，Redis有list结构，可以用作消息队列，以此提高消息可靠性。一般做法是生产者<a target="_blank" rel="noopener" href="https://redis.io/commands/lpush">LPUSH</a>消息，消费者<a target="_blank" rel="noopener" href="https://redis.io/commands/brpop">BRPOP</a>消息。适用于要求消息可靠性的简单通信场景，但缺点是消息不具状态，且没有ACK机制，无法满足复杂的通信需求</p>
<p>P.S.Redis的Pub/Sub示例见<a target="_blank" rel="noopener" href="https://stackoverflow.com/questions/6463945/whats-the-most-efficient-node-js-inter-process-communication-library-method">What’s the most efficient node.js inter-process communication library/method?</a></p>
<h2 id="17-4-总结"><a href="#17-4-总结" class="headerlink" title="17.4 总结"></a>17.4 总结</h2><p>Node进程间通信有4种方式：</p>
<ul>
<li>通过stdin/stdout传递json：最直接的方式，适用于能够拿到“子”进程handle的场景，适用于关联进程之间通信，无法跨机器</li>
<li>Node原生IPC支持：最native（地道？）的方式，比上一种“正规”一些，具有同样的局限性</li>
<li>通过sockets：最通用的方式，有良好的跨环境能力，但存在网络的性能损耗</li>
<li>借助message queue：最强大的方式，既然要通信，场景还复杂，不妨扩展出一层消息中间件，漂亮地解决各种通信问题</li>
</ul>
<h2 id="参考资料"><a href="#参考资料" class="headerlink" title="参考资料"></a>参考资料</h2><ul>
<li><a target="_blank" rel="noopener" href="https://medium.freecodecamp.org/node-js-child-processes-everything-you-need-to-know-e69498fe970a">Node.js Child Processes: Everything you need to know</a></li>
</ul>
<h1 id="18-Node-js-捕获异常"><a href="#18-Node-js-捕获异常" class="headerlink" title="18.Node.js 捕获异常"></a>18.Node.js 捕获异常</h1><p>采用事件轮训、异步 IO 等机制使得 Node.js 能够从容应对无阻塞高并发场景，令工程师很困扰的几个理解 Node.js 的地方除了它的事件（回调）机制，还有一个同样头痛的是异常代码的捕获。</p>
<h2 id="18-1-try-catch-之痛"><a href="#18-1-try-catch-之痛" class="headerlink" title="18.1 try/catch 之痛"></a>18.1 try/catch 之痛</h2><p>一般情况下，我们会将有可能出错的代码放到 <code>try/catch</code> 块里。但是到了 Node.js，由于 <code>try/catch</code> 无法捕捉异步回调里的异常，Node.js 原生提供 <code>uncaughtException</code> 事件挂到 <code>process</code> 对象上，用于捕获所有未处理的异常：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line">process.on(&#39;uncaughtException&#39;, function(err) &#123;</span><br><span class="line">    console.error(&#39;Error caught in uncaughtException event:&#39;, err);</span><br><span class="line">&#125;);</span><br><span class="line"> </span><br><span class="line">try &#123;</span><br><span class="line">    process.nextTick(function() &#123;</span><br><span class="line">        fs.readFile(&#39;non_existent.js&#39;, function(err, str) &#123;</span><br><span class="line">            if(err) throw err;</span><br><span class="line">            else console.log(str);</span><br><span class="line">        &#125;);</span><br><span class="line">    &#125;);</span><br><span class="line">&#125; catch(e) &#123;</span><br><span class="line">    console.error(&#39;Error caught by catch block:&#39;, e);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>执行的结果是代码进到了 uncaughtException 的回调里而不是 catch 块。 uncaughtException 虽然能够捕获异常，但是此时错误的上下文已经丢失，即使看到错误也不知道哪儿报的错，定位问题非常的不利。而且一旦 uncaughtException 事件触发，整个 node 进程将 crash 掉，如果不做一些善后处理的话会导致整个服务挂掉，这对于线上的服务来说将是非常不好的。</p>
<h2 id="18-2-使用-domain-模块捕捉异常"><a href="#18-2-使用-domain-模块捕捉异常" class="headerlink" title="18.2 使用 domain 模块捕捉异常"></a>18.2 使用 domain 模块捕捉异常</h2><p>随 Node.js v0.8 版本发布了一个 <a target="_blank" rel="noopener" href="http://nodejs.org/api/domain.html">domain</a>（域）模块，专门用于处理异步回调的异常，使用 <code>domain</code> 我们将很轻松的捕获异步异常：</p>
<p>运行上面的代码，我们会看到错误被 domain 捕获到，并且 uncaughtException 回调并不会执行，事情似乎变得稍微容易些了。</p>
<p>但是如果研究 domain 模块的 API 很快我们会发现，domain 提供了好几个方法，理解起来似乎不是那么直观（其实为啥这个模块叫 “域 (domain)” 呢，总感觉些许别扭），这里简单解释下：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line">process.on(&#39;uncaughtException&#39;, function(err) &#123;</span><br><span class="line">    console.error(&#39;Error caught in uncaughtException event:&#39;, err);</span><br><span class="line">&#125;);</span><br><span class="line"> </span><br><span class="line">var d &#x3D; domain.create();</span><br><span class="line"> </span><br><span class="line">d.on(&#39;error&#39;, function(err) &#123;</span><br><span class="line">    console.error(&#39;Error caught by domain:&#39;, err);</span><br><span class="line">&#125;);</span><br><span class="line"> </span><br><span class="line">d.run(function() &#123;</span><br><span class="line">    process.nextTick(function() &#123;</span><br><span class="line">        fs.readFile(&#39;non_existent.js&#39;, function(err, str) &#123;</span><br><span class="line">            if(err) throw err;</span><br><span class="line">            else console.log(str);</span><br><span class="line">        &#125;);</span><br><span class="line">    &#125;);</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure>

<p> 首先，关于 domain 模块，我们看到它的稳定性是 2，也就是不稳定，API 可能会变更。</p>
<p>默认情况下，domain 模块是不被引入的，当 <code>domain.create()</code>创建一个 domain 之后，调用 <code>enter()</code>方法即可 “激活” 这个 domain，具体表现为全局的进程（<code>process</code>）对象上会有一个 domain 属性指向之前创建的这个的 domain 实例，同时，domain 模块上有个 <code>active</code> 属性也指向这个的 domain 实例。、</p>
<p>结合 <a target="_blank" rel="noopener" href="https://github.com/visionmedia/should.js">should</a> 断言库测试下上面说的：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line">&#x2F;&#x2F; domain was not exists by default</span><br><span class="line">should.not.exist(process.domain);</span><br><span class="line"> </span><br><span class="line">var d &#x3D; domain.create();</span><br><span class="line"> </span><br><span class="line">d.on(&#39;error&#39;, function(err) &#123;</span><br><span class="line">    console.log(err);</span><br><span class="line">&#125;);</span><br><span class="line"> </span><br><span class="line">d.enter(); &#x2F;&#x2F; makes d the current domain</span><br><span class="line"> </span><br><span class="line">process.domain.should.be.an.Object;</span><br><span class="line">process.domain.should.equal(domain.active);</span><br><span class="line"> </span><br><span class="line">d.exit(); &#x2F;&#x2F; makes d inactive</span><br><span class="line"> </span><br><span class="line">should.not.exist(process.domain);</span><br></pre></td></tr></table></figure>

<p> 执行之后发现几个断言都能 pass。<code>exit()</code>方法的意思是退出当前 “域”，将会影响到后续异步异常的捕获，后面会提到。</p>
<p><code>enter</code> 和 <code>exit</code> 组合调用这样会使代码有些混乱，尤其是当多个 domain 混合、嵌套使用时比较难理解。</p>
<p>这时候可以使用 <code>run()</code>方法，<code>run()</code>其实就是对 <code>enter</code> 和 <code>exit</code> 以及回调的简单封装，即：run() – callback() – exit() 这样，就像上面例子中的 <code>run()</code>一样。</p>
<p>还有两个方法，<code>bind()</code>和 <code>intercept()</code>：</p>
<p>bind:</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">fs.readFile(&#39;non_existent.js&#39;, d.bind(function(err, buf) &#123;</span><br><span class="line">    if(err) throw err;</span><br><span class="line">    else res.end(buf.toString());</span><br><span class="line">&#125;));</span><br></pre></td></tr></table></figure>

<p>intercept：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">fs.readFile(&#39;non_existent.js&#39;, d.intercept(function(buf) &#123;</span><br><span class="line">    console.log(buf);</span><br><span class="line">&#125;));</span><br></pre></td></tr></table></figure>

<p> 用法差不多，只是 intercept 拦截了异步回调，如果抛出异常就自己处理掉了。</p>
<h3 id="18-2-1-domain-的隐式绑定"><a href="#18-2-1-domain-的隐式绑定" class="headerlink" title="18.2.1 domain 的隐式绑定"></a>18.2.1 domain 的隐式绑定</h3><p>domain 主要会影响 <code>timers</code> 模块（包括 <code>setTimeout</code>, <code>setInterval</code>, <code>setImmediate</code>）, 事件循环 <code>process.nextTick</code>，还有就是 event。</p>
<p>实现的思路都差不多，都是通过注入 domain 代码到 timer、nextTick、event 模块中，在创建的时候检查当前有没有激活（active）的 domain，有则记录下，如果是 timer 和 nextTick，当在事件循环中执行回调的时候，把 process.domain 设置为之前记录的 domain 并把错误交给它处理。如果是 event，多一步判断，先会把异常交给 event 自己定义的 error 事件处理。</p>
<p>这里要注意，如果这个 domain 没有绑定 <code>error</code> 事件的话，node 会直接抛出错误，即使 uncaughtException 绑定了也没有用：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line">process.on(&#39;uncaughtException&#39;, function(err) &#123;</span><br><span class="line">    console.error(&#39;Error caught in uncaughtException event:&#39;, err);</span><br><span class="line">&#125;);</span><br><span class="line"> </span><br><span class="line">var d &#x3D; domain.create();</span><br><span class="line"> </span><br><span class="line">d.on(&#39;error&#39;, function(err) &#123;</span><br><span class="line">    console.error(&#39;Error caught by domain:&#39;, err);</span><br><span class="line">&#125;);</span><br><span class="line"> </span><br><span class="line">d.run(function() &#123;</span><br><span class="line">    process.nextTick(function() &#123;</span><br><span class="line">        fs.readFile(&#39;non_existent.js&#39;, function(err, str) &#123;</span><br><span class="line">            if(err) throw err;</span><br><span class="line">            else console.log(str);</span><br><span class="line">        &#125;);</span><br><span class="line">    &#125;);</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure>

<p> 在这个例子里面，使用了 domain 捕获异常但是没有监听 domain 的 error 事件，监听了 uncaughtException，但是还是抛出了异常，个人觉得觉得这里是个 bug，domain 没有 errorHandle 应该把异常交给全局的 uncaughtException，后面有例子验证这一点。</p>
<p>还有一个小问题，同时监听了 uncaughtException 和 domain 的 error 事件，在 node v0.8 里有个 bug，uncaughtException 和 domain 都能捕获异常，0.10+已经修复。</p>
<h3 id="18-2-2-domain-的显式绑定"><a href="#18-2-2-domain-的显式绑定" class="headerlink" title="18.2.2 domain 的显式绑定"></a>18.2.2 domain 的显式绑定</h3><p>上面没有提到的两个 API 是 <code>add()</code>和 <code>remove()</code>，add 作用是把 domain 创建之前创建的（EventEmitter 实例）对象添加到这个 domain 里边，然后这个对象即可使用 domain 捕捉异常了，remove 则相反。domain 对象上有个 numbers 队列专门用于管理 add 后的对象。</p>
<p>这里可参考<a target="_blank" rel="noopener" href="http://nodejs.org/api/domain.html#domain_explicit_binding">官方示例</a>。</p>
<h3 id="18-2-3-domain-如何抛出异常"><a href="#18-2-3-domain-如何抛出异常" class="headerlink" title="18.2.3 domain 如何抛出异常"></a>18.2.3 domain 如何抛出异常</h3><p>我们看 <a target="_blank" rel="noopener" href="https://github.com/joyent/node/blob/v0.10.4/src/node.js#L43">node 源码</a>有这么一行：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">&#x2F;&#x2F; do this good and early, since it handles errors.</span><br><span class="line">startup.processFatal();</span><br></pre></td></tr></table></figure>

<p> <code>processFatal</code> 里边调用 <code>process._fatalException()</code>，先判断是否存在 process.domain，尝试把错误交给 process.domain 处理，如果不存在才交给 uncaughtException 处理，所以 domain 捕获异常的关键代码在 <a target="_blank" rel="noopener" href="https://github.com/joyent/node/blob/v0.10.4/src/node.js#L219">node.js#L219</a>。</p>
<p>这里尝试修改下上面的例子，在抛出异常前把 process.domain 设为 null：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">d.run(function() &#123;</span><br><span class="line">    process.domain &#x3D; null;</span><br><span class="line">    process.nextTick(function() &#123;</span><br><span class="line">        fs.readFile(&#39;non_existent.js&#39;, function(err, str) &#123;</span><br><span class="line">            if(err) throw err;</span><br><span class="line">            else console.log(str);</span><br><span class="line">        &#125;);</span><br><span class="line">    &#125;);</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure>

<p>这下 uncaughtException 将捕获异常！</p>
<p>当上面提到的异常都没被捕获，进程将直接退出 <a target="_blank" rel="noopener" href="https://github.com/joyent/node/blob/v0.10.4/src/node.js#L280">node.js#L280</a>：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">&#x2F;&#x2F; if someone handled it, then great.  otherwise, die in C++ land</span><br><span class="line">&#x2F;&#x2F; since that means that we&#39;ll exit the process, emit the &#39;exit&#39; event</span><br><span class="line">...</span><br><span class="line">process.emit(&#39;exit&#39;, 1);</span><br></pre></td></tr></table></figure>

<p>另外关于 domain 如何在多个不同的事件循环中传递，可以参考下<a target="_blank" rel="noopener" href="http://deadhorse.me/nodejs/2013/04/13/exception_and_domain.html">这篇</a>文章。</p>
<p>值得关注的是，并不是所有在 domain 域下创建的事件分发器（EventEmitter）上面的异步异常都能捕获：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br></pre></td><td class="code"><pre><span class="line">var d &#x3D; domain.create();</span><br><span class="line">var msg;</span><br><span class="line">var Msg &#x3D; function() &#123;</span><br><span class="line">    events.EventEmitter.call(this);</span><br><span class="line"> </span><br><span class="line">    this.on(&#39;msg&#39;, function(msg) &#123;</span><br><span class="line">        console.log(msg);</span><br><span class="line">    &#125;);</span><br><span class="line"> </span><br><span class="line">    this.send &#x3D; function(msg) &#123;</span><br><span class="line">        this.emit(&#39;msg&#39;, msg);</span><br><span class="line">    &#125;;</span><br><span class="line"> </span><br><span class="line">    this.read &#x3D; function(file) &#123;</span><br><span class="line">        var root &#x3D; this;</span><br><span class="line">        fs.readFile(file, function(err, buf) &#123;</span><br><span class="line">            if(err) throw err;</span><br><span class="line">            else root.send(buf.toString());</span><br><span class="line">        &#125;);</span><br><span class="line">    &#125;;</span><br><span class="line">&#125;;</span><br><span class="line"> </span><br><span class="line">require(&#39;util&#39;).inherits(Msg, events.EventEmitter);</span><br><span class="line"> </span><br><span class="line">d.on(&#39;error&#39;, function(err) &#123;</span><br><span class="line">    console.error(&#39;Error caught by domain:&#39;, err);</span><br><span class="line">&#125;);</span><br><span class="line"> </span><br><span class="line">d.run(function() &#123;</span><br><span class="line">    msg &#x3D; new Msg();</span><br><span class="line">&#125;);</span><br><span class="line"> </span><br><span class="line">msg.read(&#39;non_existent.js&#39;);</span><br></pre></td></tr></table></figure>

<p> 这个例子中，msg 对象虽然是在 domain 中实例化，但是 msg.send 里边 fs.readFile 在执行回调的时候，process.domain 是 <code>undefined</code>。</p>
<p>我们稍微改造下，把 readFile 的回调绑定到 domain 上，或者把 msg.send() 的调用放到 d.run() 包裹，结果可预知，能正常捕获抛出的异常。为了验证，尝试改造下 readFile：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">fs.readFile(file, function(err, buf) &#123;</span><br><span class="line">    process.domain &#x3D; d;</span><br><span class="line">    if(err) throw err;</span><br><span class="line">    else root.send(buf.toString());</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure>

<p> 这样亦可捕获异常，不过实际中不要这样写，还是要采用 domain 提供的方法。</p>
<h4 id="更好的使用-domain"><a href="#更好的使用-domain" class="headerlink" title="更好的使用 domain"></a>更好的使用 domain</h4><p>其实上，更推荐的做法是，如果在活动 domain 里面创建了事件分发器（EventEmitter）实例，我们应该尽可能的给它注册 error 事件，把错误都抛给这个 EventEmitter 实例处理，就像上面的例子，我们改造下，绑定 error 事件并把 readFile 的错误交给 Msg 实例处理：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">this.on(&#39;error&#39;, function(err) &#123;</span><br><span class="line">    throw err;</span><br><span class="line">&#125;);</span><br><span class="line"> </span><br><span class="line">this.read &#x3D; function(file) &#123;</span><br><span class="line">    var root &#x3D; this;</span><br><span class="line">    fs.readFile(file, function(err, buf) &#123;</span><br><span class="line">        if(err) root.emit(&#39;error&#39;, err);</span><br><span class="line">        else root.send(buf.toString());</span><br><span class="line">    &#125;);</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure>

<p> 在书写 Node.js 代码的时候，对于事件分发器，应该养成先绑定（<code>on()</code>或 <code>addEventListener()</code>）后触发（<code>emit()</code>）的习惯。在执行事件回调的时候，<strong>对于有可能抛异常的情况，应该把 emit 放到 domain 里去</strong>： </p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line">var d &#x3D; domain.create();</span><br><span class="line">var e &#x3D; new events.EventEmitter();</span><br><span class="line"> </span><br><span class="line">d.on(&#39;error&#39;, function(err) &#123;</span><br><span class="line">    console.error(&#39;Error caught by domain:&#39;, err);</span><br><span class="line">&#125;);</span><br><span class="line"> </span><br><span class="line">e.on(&#39;data&#39;, function(err) &#123;</span><br><span class="line">    if(err) throw err;</span><br><span class="line">&#125;);</span><br><span class="line"> </span><br><span class="line">if(Math.random() &gt; 0.5) &#123;</span><br><span class="line">    d.run(function() &#123;</span><br><span class="line">        e.emit(&#39;data&#39;, new Error(&#39;Error in domain runtime.&#39;));</span><br><span class="line">    &#125;);</span><br><span class="line">&#125; else &#123;</span><br><span class="line">    e.emit(&#39;data&#39;, new Error(&#39;Error without domain.&#39;));</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>根据 <a target="_blank" rel="noopener" href="https://github.com/joyent/node/blob/v0.10.4/lib/domain.js#L187">domain#L187</a> 可知，run 会把传进去的函数包装成另一个函数返回，并在这个返回的函数上设置 domain：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">b.domain &#x3D; this;</span><br></pre></td></tr></table></figure>

<p> events 模块 <a target="_blank" rel="noopener" href="https://github.com/joyent/node/blob/v0.10.4/lib/events.js#L85">events.js#L85</a> 有这么一行：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"></span><br></pre></td></tr></table></figure>

<p>当调用 e.emit() 的时候，如果回调函数上挂有 domain，则将这个 domain 激活，进而可以捕获异常。</p>
<h4 id="domain-的缺陷"><a href="#domain-的缺陷" class="headerlink" title="domain 的缺陷"></a>domain 的缺陷</h4><p>有了 domain，似乎异步异常捕捉已经不再是难事。Node.js 允许创建多个 domain 实例，并允许使用 add 添加多个事件分发器给 domain 管理，，而且 domain 之间可以相互嵌套，而创建 domain，是有一定的性能耗损的，这样带来了一个棘手的问题是：多个 domain 如何合理的创建与销毁，domain 的运行期应该如何维护？</p>
<p>还有一点，domain 并不能捕捉所有的异常，看<a target="_blank" rel="noopener" href="https://github.com/domenic/domains-tragedy">这里</a>。</p>
<h4 id="domain-实践"><a href="#domain-实践" class="headerlink" title="domain 实践"></a>domain 实践</h4><p>关于使用 domain 到集群环境，推荐都看看官方的说明：<a target="_blank" rel="noopener" href="http://nodejs.org/docs/latest/api/domain.html#domain_warning_don_t_ignore_errors">Warning: Don’t Ignore Errors!</a>。把每一个网络请求都包在一个 domain 里边，捕获到异常时，不要立即退出进程，应该保证进程中其他连接正常退出之后再 exit，官方推荐的是设一个定时器，过 3min 后退出进程，接下去做善后处理，然后应该返回应该有的错误（如 500）给客户端。</p>
<p>对于 connect 或者 express 创建的 web 服务，有一个 <a target="_blank" rel="noopener" href="https://github.com/fengmk2/domain-middleware">domain-middleware</a> 中间件可以直接用，它会把 next 包装到一个已经定制好的 domain 里边。</p>
<p>在具体应用场景，应该 uncaughtException 事件配合 domain 来用。</p>
<p>本篇完，欢迎补充指正，所有用到的例子都在<a target="_blank" rel="noopener" href="https://github.com/chemdemo/chemdemo.github.io/blob/master/demos/domain_demo.js">这里</a>。</p>
<p>参考资料：</p>
<ul>
<li><a target="_blank" rel="noopener" href="http://nodejs.org/docs/latest/api/domain.html">http://nodejs.org/docs/latest/api/domain.html</a></li>
<li><a target="_blank" rel="noopener" href="https://github.com/joyent/node">https://github.com/joyent/node</a></li>
<li><a target="_blank" rel="noopener" href="http://www.slideshare.net/domenicdenicola/domains-20010482">http://www.slideshare.net/domenicdenicola/domains-20010482</a></li>
<li><a target="_blank" rel="noopener" href="http://deadhorse.me/nodejs/2013/04/13/exception_and_domain.html">http://deadhorse.me/nodejs/2013/04/13/exception_and_domain.html</a></li>
</ul>

    </div>

    
    
    
      
<div>
        <div style="text-align:center;color: #ccc;font-size:14px;">-------------本文结束<i class="fa fa-paw"></i>感谢您的阅读-------------</div>
</div>
        

  <div class="followme">
    <p>欢迎关注我的其它发布渠道</p>

    <div class="social-list">

        <div class="social-item">
          <a target="_blank" class="social-link" href="/atom.xml">
            <span class="icon">
              <i class="fa fa-rss"></i>
            </span>

            <span class="label">RSS</span>
          </a>
        </div>
    </div>
  </div>


      <footer class="post-footer">
          <div class="post-tags">
              <a href="/tags/%E9%9D%A2%E8%AF%95/" rel="tag"><i class="fa fa-tag"></i> 面试</a>
              <a href="/tags/javascript/" rel="tag"><i class="fa fa-tag"></i> javascript</a>
              <a href="/tags/Node/" rel="tag"><i class="fa fa-tag"></i> Node</a>
          </div>

        


        
    <div class="post-nav">
      <div class="post-nav-item">
    <a href="/2021/03/14/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C/" rel="prev" title="计算机网络">
      <i class="fa fa-chevron-left"></i> 计算机网络
    </a></div>
      <div class="post-nav-item">
    <a href="/2021/03/21/%E5%88%86%E4%BA%AB%E4%B8%80%E4%BA%9B%20Chrome%20%E6%B5%8F%E8%A7%88%E5%99%A8%E7%9A%84%E5%89%8D%E7%AB%AF%E8%B0%83%E8%AF%95%E6%8A%80%E5%B7%A7/" rel="next" title="分享一些 Chrome 浏览器的前端调试技巧">
      分享一些 Chrome 浏览器的前端调试技巧 <i class="fa fa-chevron-right"></i>
    </a></div>
    </div>
      </footer>
    
  </article>
  
  
  



          </div>
          
    <div class="comments" id="valine-comments"></div>

<script>
  window.addEventListener('tabs:register', () => {
    let { activeClass } = CONFIG.comments;
    if (CONFIG.comments.storage) {
      activeClass = localStorage.getItem('comments_active') || activeClass;
    }
    if (activeClass) {
      let activeTab = document.querySelector(`a[href="#comment-${activeClass}"]`);
      if (activeTab) {
        activeTab.click();
      }
    }
  });
  if (CONFIG.comments.storage) {
    window.addEventListener('tabs:click', event => {
      if (!event.target.matches('.tabs-comment .tab-content .tab-pane')) return;
      let commentClass = event.target.classList[1];
      localStorage.setItem('comments_active', commentClass);
    });
  }
</script>

        </div>
          
  
  <div class="toggle sidebar-toggle">
    <span class="toggle-line toggle-line-first"></span>
    <span class="toggle-line toggle-line-middle"></span>
    <span class="toggle-line toggle-line-last"></span>
  </div>

  <aside class="sidebar">
    <div class="sidebar-inner">

      <ul class="sidebar-nav motion-element">
        <li class="sidebar-nav-toc">
          文章目录
        </li>
        <li class="sidebar-nav-overview">
          站点概览
        </li>
      </ul>

      <!--noindex-->
      <div class="post-toc-wrap sidebar-panel">
          <div class="post-toc motion-element"><ol class="nav"><li class="nav-item nav-level-1"><a class="nav-link" href="#1-%E6%B5%8F%E8%A7%88%E5%99%A8%E4%B8%8ENode%E7%9A%84%E4%BA%8B%E4%BB%B6%E5%BE%AA%E7%8E%AF-Event-Loop-%E6%9C%89%E4%BD%95%E5%8C%BA%E5%88%AB"><span class="nav-text">1. 浏览器与Node的事件循环(Event Loop)有何区别?</span></a><ol class="nav-child"><li class="nav-item nav-level-2"><a class="nav-link" href="#1-1-%E7%BA%BF%E7%A8%8B%E4%B8%8E%E8%BF%9B%E7%A8%8B"><span class="nav-text">1.1 线程与进程</span></a><ol class="nav-child"><li class="nav-item nav-level-3"><a class="nav-link" href="#1-%E6%A6%82%E5%BF%B5"><span class="nav-text">1.概念</span></a></li><li class="nav-item nav-level-3"><a class="nav-link" href="#2-%E5%A4%9A%E8%BF%9B%E7%A8%8B%E4%B8%8E%E5%A4%9A%E7%BA%BF%E7%A8%8B"><span class="nav-text">2.多进程与多线程</span></a></li></ol></li><li class="nav-item nav-level-2"><a class="nav-link" href="#1-2-%E6%B5%8F%E8%A7%88%E5%99%A8%E5%86%85%E6%A0%B8"><span class="nav-text">1.2 浏览器内核</span></a><ol class="nav-child"><li class="nav-item nav-level-3"><a class="nav-link" href="#1-2-1-GUI%E6%B8%B2%E6%9F%93%E7%BA%BF%E7%A8%8B"><span class="nav-text">1.2.1 GUI渲染线程</span></a></li><li class="nav-item nav-level-3"><a class="nav-link" href="#1-2-2-JS%E5%BC%95%E6%93%8E%E7%BA%BF%E7%A8%8B"><span class="nav-text">1.2.2 JS引擎线程</span></a></li><li class="nav-item nav-level-3"><a class="nav-link" href="#1-2-3-%E5%AE%9A%E6%97%B6%E5%99%A8%E8%A7%A6%E5%8F%91%E7%BA%BF%E7%A8%8B"><span class="nav-text">1.2.3 定时器触发线程</span></a></li><li class="nav-item nav-level-3"><a class="nav-link" href="#1-2-4%E4%BA%8B%E4%BB%B6%E8%A7%A6%E5%8F%91%E7%BA%BF%E7%A8%8B"><span class="nav-text">1.2.4事件触发线程</span></a></li><li class="nav-item nav-level-3"><a class="nav-link" href="#1-2-5-%E5%BC%82%E6%AD%A5http%E8%AF%B7%E6%B1%82%E7%BA%BF%E7%A8%8B"><span class="nav-text">1.2.5 异步http请求线程</span></a></li></ol></li><li class="nav-item nav-level-2"><a class="nav-link" href="#1-3-%E6%B5%8F%E8%A7%88%E5%99%A8%E4%B8%AD%E7%9A%84-Event-Loop"><span class="nav-text">1.3 浏览器中的 Event Loop</span></a><ol class="nav-child"><li class="nav-item nav-level-3"><a class="nav-link" href="#1-3-1-Micro-Task-%E4%B8%8E-Macro-Task"><span class="nav-text">1.3.1 Micro-Task 与 Macro-Task</span></a></li><li class="nav-item nav-level-3"><a class="nav-link" href="#1-3-2-Event-Loop-%E8%BF%87%E7%A8%8B%E8%A7%A3%E6%9E%90"><span class="nav-text">1.3.2 Event Loop 过程解析</span></a></li></ol></li><li class="nav-item nav-level-2"><a class="nav-link" href="#1-4-Node-%E4%B8%AD%E7%9A%84-Event-Loop"><span class="nav-text">1.4 Node 中的 Event Loop</span></a><ol class="nav-child"><li class="nav-item nav-level-3"><a class="nav-link" href="#1-4-1-Node%E7%AE%80%E4%BB%8B"><span class="nav-text">1.4.1 Node简介</span></a></li><li class="nav-item nav-level-3"><a class="nav-link" href="#1-4-2-%E5%85%AD%E4%B8%AA%E9%98%B6%E6%AE%B5"><span class="nav-text">1.4.2 六个阶段</span></a><ol class="nav-child"><li class="nav-item nav-level-4"><a class="nav-link" href="#1-timer"><span class="nav-text">(1) timer</span></a></li><li class="nav-item nav-level-4"><a class="nav-link" href="#2-poll"><span class="nav-text">(2) poll</span></a></li><li class="nav-item nav-level-4"><a class="nav-link" href="#3-check%E9%98%B6%E6%AE%B5"><span class="nav-text">(3) check阶段</span></a></li></ol></li><li class="nav-item nav-level-3"><a class="nav-link" href="#1-4-3-Micro-Task-%E4%B8%8E-Macro-Task"><span class="nav-text">1.4.3 Micro-Task 与 Macro-Task</span></a></li><li class="nav-item nav-level-3"><a class="nav-link" href="#1-4-4-%E6%B3%A8%E6%84%8F%E7%82%B9"><span class="nav-text">1.4.4 注意点</span></a><ol class="nav-child"><li class="nav-item nav-level-4"><a class="nav-link" href="#1-setTimeout-%E5%92%8C-setImmediate"><span class="nav-text">(1) setTimeout 和 setImmediate</span></a></li><li class="nav-item nav-level-4"><a class="nav-link" href="#2-process-nextTick"><span class="nav-text">(2) process.nextTick</span></a></li></ol></li></ol></li><li class="nav-item nav-level-2"><a class="nav-link" href="#1-5-Node%E4%B8%8E%E6%B5%8F%E8%A7%88%E5%99%A8%E7%9A%84-Event-Loop-%E5%B7%AE%E5%BC%82"><span class="nav-text">1.5 Node与浏览器的 Event Loop 差异</span></a></li><li class="nav-item nav-level-2"><a class="nav-link" href="#1-6-%E6%80%BB%E7%BB%93"><span class="nav-text">1.6 总结</span></a></li><li class="nav-item nav-level-2"><a class="nav-link" href="#%E5%90%8E%E8%AE%B0"><span class="nav-text">后记</span></a></li><li class="nav-item nav-level-2"><a class="nav-link" href="#%E5%8F%82%E8%80%83%E6%96%87%E7%AB%A0"><span class="nav-text">参考文章</span></a></li></ol></li><li class="nav-item nav-level-1"><a class="nav-link" href="#2-%E5%89%8D%E7%AB%AF%E6%A8%A1%E5%9D%97%E5%8C%96"><span class="nav-text">2.前端模块化</span></a><ol class="nav-child"><li class="nav-item nav-level-2"><a class="nav-link" href="#%E5%89%8D%E8%A8%80"><span class="nav-text">前言</span></a></li><li class="nav-item nav-level-2"><a class="nav-link" href="#2-1-%E6%A8%A1%E5%9D%97%E5%8C%96%E7%9A%84%E7%90%86%E8%A7%A3"><span class="nav-text">2.1 模块化的理解</span></a><ol class="nav-child"><li class="nav-item nav-level-3"><a class="nav-link" href="#2-1-1-%E4%BB%80%E4%B9%88%E6%98%AF%E6%A8%A1%E5%9D%97"><span class="nav-text">2.1.1 什么是模块?</span></a></li><li class="nav-item nav-level-3"><a class="nav-link" href="#2-2-2-%E6%A8%A1%E5%9D%97%E5%8C%96%E7%9A%84%E8%BF%9B%E5%8C%96%E8%BF%87%E7%A8%8B"><span class="nav-text">2.2.2 模块化的进化过程</span></a></li><li class="nav-item nav-level-3"><a class="nav-link" href="#2-2-3-%E6%A8%A1%E5%9D%97%E5%8C%96%E7%9A%84%E5%A5%BD%E5%A4%84"><span class="nav-text">2.2.3 模块化的好处</span></a></li><li class="nav-item nav-level-3"><a class="nav-link" href="#2-2-4-%E5%BC%95%E5%85%A5%E5%A4%9A%E4%B8%AA-lt-script-gt-%E5%90%8E%E5%87%BA%E7%8E%B0%E5%87%BA%E7%8E%B0%E9%97%AE%E9%A2%98"><span class="nav-text">2.2.4 引入多个&lt;script&gt;后出现出现问题</span></a></li></ol></li><li class="nav-item nav-level-2"><a class="nav-link" href="#2-2-%E6%A8%A1%E5%9D%97%E5%8C%96%E8%A7%84%E8%8C%83"><span class="nav-text">2.2 模块化规范</span></a><ol class="nav-child"><li class="nav-item nav-level-3"><a class="nav-link" href="#2-2-1-CommonJS"><span class="nav-text">2.2.1 CommonJS</span></a><ol class="nav-child"><li class="nav-item nav-level-4"><a class="nav-link" href="#1-%E6%A6%82%E8%BF%B0"><span class="nav-text">(1)概述</span></a></li><li class="nav-item nav-level-4"><a class="nav-link" href="#2-%E7%89%B9%E7%82%B9"><span class="nav-text">(2)特点</span></a></li><li class="nav-item nav-level-4"><a class="nav-link" href="#3-%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95"><span class="nav-text">(3)基本语法</span></a></li><li class="nav-item nav-level-4"><a class="nav-link" href="#4-%E6%A8%A1%E5%9D%97%E7%9A%84%E5%8A%A0%E8%BD%BD%E6%9C%BA%E5%88%B6"><span class="nav-text">(4)模块的加载机制</span></a></li><li class="nav-item nav-level-4"><a class="nav-link" href="#5-%E6%9C%8D%E5%8A%A1%E5%99%A8%E7%AB%AF%E5%AE%9E%E7%8E%B0"><span class="nav-text">(5)服务器端实现</span></a></li><li class="nav-item nav-level-4"><a class="nav-link" href="#%E2%91%A0%E4%B8%8B%E8%BD%BD%E5%AE%89%E8%A3%85node-js"><span class="nav-text">①下载安装node.js</span></a></li><li class="nav-item nav-level-4"><a class="nav-link" href="#%E2%91%A1%E5%88%9B%E5%BB%BA%E9%A1%B9%E7%9B%AE%E7%BB%93%E6%9E%84"><span class="nav-text">②创建项目结构</span></a></li><li class="nav-item nav-level-4"><a class="nav-link" href="#%E2%91%A2%E4%B8%8B%E8%BD%BD%E7%AC%AC%E4%B8%89%E6%96%B9%E6%A8%A1%E5%9D%97"><span class="nav-text">③下载第三方模块</span></a></li><li class="nav-item nav-level-4"><a class="nav-link" href="#%E2%91%A3%E5%AE%9A%E4%B9%89%E6%A8%A1%E5%9D%97%E4%BB%A3%E7%A0%81"><span class="nav-text">④定义模块代码</span></a></li><li class="nav-item nav-level-4"><a class="nav-link" href="#%E2%91%A4%E9%80%9A%E8%BF%87node%E8%BF%90%E8%A1%8Capp-js"><span class="nav-text">⑤通过node运行app.js</span></a></li><li class="nav-item nav-level-4"><a class="nav-link" href="#6-%E6%B5%8F%E8%A7%88%E5%99%A8%E7%AB%AF%E5%AE%9E%E7%8E%B0-%E5%80%9F%E5%8A%A9Browserify"><span class="nav-text">(6)浏览器端实现(借助Browserify)</span></a></li><li class="nav-item nav-level-4"><a class="nav-link" href="#%E2%91%A0%E5%88%9B%E5%BB%BA%E9%A1%B9%E7%9B%AE%E7%BB%93%E6%9E%84"><span class="nav-text">①创建项目结构</span></a></li><li class="nav-item nav-level-4"><a class="nav-link" href="#%E2%91%A1%E4%B8%8B%E8%BD%BDbrowserify"><span class="nav-text">②下载browserify</span></a></li><li class="nav-item nav-level-4"><a class="nav-link" href="#%E2%91%A2%E5%AE%9A%E4%B9%89%E6%A8%A1%E5%9D%97%E4%BB%A3%E7%A0%81-%E5%90%8C%E6%9C%8D%E5%8A%A1%E5%99%A8%E7%AB%AF"><span class="nav-text">③定义模块代码(同服务器端)</span></a></li><li class="nav-item nav-level-4"><a class="nav-link" href="#%E2%91%A3%E6%89%93%E5%8C%85%E5%A4%84%E7%90%86js"><span class="nav-text">④打包处理js</span></a></li><li class="nav-item nav-level-4"><a class="nav-link" href="#%E2%91%A4%E9%A1%B5%E9%9D%A2%E4%BD%BF%E7%94%A8%E5%BC%95%E5%85%A5"><span class="nav-text">⑤页面使用引入</span></a></li></ol></li><li class="nav-item nav-level-3"><a class="nav-link" href="#2-2-2-AMD"><span class="nav-text">2.2.2 AMD</span></a><ol class="nav-child"><li class="nav-item nav-level-4"><a class="nav-link" href="#1-AMD%E8%A7%84%E8%8C%83%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95"><span class="nav-text">(1)AMD规范基本语法</span></a></li><li class="nav-item nav-level-4"><a class="nav-link" href="#2-%E6%9C%AA%E4%BD%BF%E7%94%A8AMD%E8%A7%84%E8%8C%83%E4%B8%8E%E4%BD%BF%E7%94%A8require-js"><span class="nav-text">(2)未使用AMD规范与使用require.js</span></a></li><li class="nav-item nav-level-4"><a class="nav-link" href="#%E2%91%A0%E4%B8%8B%E8%BD%BDrequire-js-%E5%B9%B6%E5%BC%95%E5%85%A5"><span class="nav-text">①下载require.js, 并引入</span></a></li><li class="nav-item nav-level-4"><a class="nav-link" href="#%E2%91%A1%E5%88%9B%E5%BB%BA%E9%A1%B9%E7%9B%AE%E7%BB%93%E6%9E%84-1"><span class="nav-text">②创建项目结构</span></a></li><li class="nav-item nav-level-4"><a class="nav-link" href="#%E2%91%A2%E5%AE%9A%E4%B9%89require-js%E7%9A%84%E6%A8%A1%E5%9D%97%E4%BB%A3%E7%A0%81"><span class="nav-text">③定义require.js的模块代码</span></a></li><li class="nav-item nav-level-4"><a class="nav-link" href="#%E2%91%A3%E9%A1%B5%E9%9D%A2%E5%BC%95%E5%85%A5require-js%E6%A8%A1%E5%9D%97"><span class="nav-text">④页面引入require.js模块:</span></a></li></ol></li><li class="nav-item nav-level-3"><a class="nav-link" href="#2-2-3-CMD"><span class="nav-text">2.2.3 CMD</span></a><ol class="nav-child"><li class="nav-item nav-level-4"><a class="nav-link" href="#1-CMD%E8%A7%84%E8%8C%83%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95"><span class="nav-text">(1)CMD规范基本语法</span></a></li><li class="nav-item nav-level-4"><a class="nav-link" href="#2-sea-js%E7%AE%80%E5%8D%95%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B"><span class="nav-text">(2)sea.js简单使用教程</span></a></li><li class="nav-item nav-level-4"><a class="nav-link" href="#%E2%91%A0%E4%B8%8B%E8%BD%BDsea-js-%E5%B9%B6%E5%BC%95%E5%85%A5"><span class="nav-text">①下载sea.js, 并引入</span></a></li><li class="nav-item nav-level-4"><a class="nav-link" href="#%E2%91%A1%E5%88%9B%E5%BB%BA%E9%A1%B9%E7%9B%AE%E7%BB%93%E6%9E%84-2"><span class="nav-text">②创建项目结构</span></a></li><li class="nav-item nav-level-4"><a class="nav-link" href="#%E2%91%A2%E5%AE%9A%E4%B9%89sea-js%E7%9A%84%E6%A8%A1%E5%9D%97%E4%BB%A3%E7%A0%81"><span class="nav-text">③定义sea.js的模块代码</span></a></li><li class="nav-item nav-level-4"><a class="nav-link" href="#%E2%91%A3%E5%9C%A8index-html%E4%B8%AD%E5%BC%95%E5%85%A5"><span class="nav-text">④在index.html中引入</span></a></li></ol></li><li class="nav-item nav-level-3"><a class="nav-link" href="#2-2-4-ES6%E6%A8%A1%E5%9D%97%E5%8C%96"><span class="nav-text">2.2.4 ES6模块化</span></a><ol class="nav-child"><li class="nav-item nav-level-4"><a class="nav-link" href="#1-ES6%E6%A8%A1%E5%9D%97%E5%8C%96%E8%AF%AD%E6%B3%95"><span class="nav-text">(1)ES6模块化语法</span></a></li><li class="nav-item nav-level-4"><a class="nav-link" href="#2-ES6-%E6%A8%A1%E5%9D%97%E4%B8%8E-CommonJS-%E6%A8%A1%E5%9D%97%E7%9A%84%E5%B7%AE%E5%BC%82"><span class="nav-text">(2)ES6 模块与 CommonJS 模块的差异</span></a></li><li class="nav-item nav-level-4"><a class="nav-link" href="#3-ES6-Babel-Browserify%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B"><span class="nav-text">(3) ES6-Babel-Browserify使用教程</span></a></li><li class="nav-item nav-level-4"><a class="nav-link" href="#%E2%91%A0%E5%AE%9A%E4%B9%89package-json%E6%96%87%E4%BB%B6"><span class="nav-text">①定义package.json文件</span></a></li><li class="nav-item nav-level-4"><a class="nav-link" href="#%E2%91%A1%E5%AE%89%E8%A3%85babel-cli-babel-preset-es2015%E5%92%8Cbrowserify"><span class="nav-text">②安装babel-cli, babel-preset-es2015和browserify</span></a></li><li class="nav-item nav-level-4"><a class="nav-link" href="#%E2%91%A2%E5%AE%9A%E4%B9%89-babelrc%E6%96%87%E4%BB%B6"><span class="nav-text">③定义.babelrc文件</span></a></li><li class="nav-item nav-level-4"><a class="nav-link" href="#%E2%91%A3%E5%AE%9A%E4%B9%89%E6%A8%A1%E5%9D%97%E4%BB%A3%E7%A0%81-1"><span class="nav-text">④定义模块代码</span></a></li><li class="nav-item nav-level-4"><a class="nav-link" href="#%E2%91%A4-%E7%BC%96%E8%AF%91%E5%B9%B6%E5%9C%A8index-html%E4%B8%AD%E5%BC%95%E5%85%A5"><span class="nav-text">⑤ 编译并在index.html中引入</span></a></li></ol></li></ol></li><li class="nav-item nav-level-2"><a class="nav-link" href="#2-3-%E6%80%BB%E7%BB%93"><span class="nav-text">2.3 总结</span></a></li></ol></li><li class="nav-item nav-level-1"><a class="nav-link" href="#3-Tree-Shaking"><span class="nav-text">3.Tree-Shaking</span></a><ol class="nav-child"><li class="nav-item nav-level-2"><a class="nav-link" href="#3-1-%E4%BB%80%E4%B9%88%E6%98%AFTree-shaking"><span class="nav-text">3.1 什么是Tree-shaking</span></a></li><li class="nav-item nav-level-2"><a class="nav-link" href="#3-2-tree-shaking%E7%9A%84%E5%8E%9F%E7%90%86"><span class="nav-text">3.2 tree-shaking的原理</span></a></li><li class="nav-item nav-level-2"><a class="nav-link" href="#3-3-tree-shaking%E5%AE%9E%E8%B7%B5"><span class="nav-text">3.3 tree-shaking实践</span></a></li><li class="nav-item nav-level-2"><a class="nav-link" href="#%EF%BC%881%EF%BC%89%E5%AF%B9%E7%BB%84%E4%BB%B6%E5%BA%93%E5%BC%95%E7%94%A8%E7%9A%84%E4%BC%98%E5%8C%96"><span class="nav-text">（1）对组件库引用的优化</span></a></li><li class="nav-item nav-level-2"><a class="nav-link" href="#%EF%BC%882%EF%BC%89CSS-Tree-shaking"><span class="nav-text">（2）CSS Tree-shaking</span></a></li><li class="nav-item nav-level-2"><a class="nav-link" href="#%EF%BC%883%EF%BC%89webpack-bundle%E6%96%87%E4%BB%B6%E5%8E%BB%E9%87%8D"><span class="nav-text">（3）webpack bundle文件去重</span></a></li></ol></li><li class="nav-item nav-level-1"><a class="nav-link" href="#4-uglify%E5%8E%9F%E7%90%86"><span class="nav-text">4.uglify原理</span></a><ol class="nav-child"><li class="nav-item nav-level-2"><a class="nav-link" href="#4-1-AST%EF%BC%88%E6%8A%BD%E8%B1%A1%E8%AF%AD%E6%B3%95%E6%A0%91%EF%BC%89"><span class="nav-text">4.1  AST（抽象语法树）</span></a></li><li class="nav-item nav-level-2"><a class="nav-link" href="#4-2-%E4%BB%A3%E7%A0%81%E5%8E%8B%E7%BC%A9%E5%8E%9F%E7%90%86"><span class="nav-text">4.2 代码压缩原理</span></a></li><li class="nav-item nav-level-2"><a class="nav-link" href="#4-3-%E4%BB%A3%E7%A0%81%E5%8E%8B%E7%BC%A9%E8%A7%84%E5%88%99"><span class="nav-text">4.3 代码压缩规则</span></a></li><li class="nav-item nav-level-2"><a class="nav-link" href="#4-4-%E6%80%BB%E7%BB%93"><span class="nav-text">4.4 总结</span></a></li><li class="nav-item nav-level-2"><a class="nav-link" href="#%E5%8F%82%E8%80%83%E6%96%87%E7%8C%AE"><span class="nav-text">参考文献</span></a></li></ol></li><li class="nav-item nav-level-1"><a class="nav-link" href="#5-Babel%E5%8E%9F%E7%90%86"><span class="nav-text">5.Babel原理</span></a><ol class="nav-child"><li class="nav-item nav-level-2"><a class="nav-link" href="#5-1-%E4%BB%80%E4%B9%88%E6%98%AF-AST"><span class="nav-text">5.1 什么是 AST</span></a></li><li class="nav-item nav-level-2"><a class="nav-link" href="#5-2-%E8%AF%8D%E6%B3%95%E5%88%86%E6%9E%90%E5%92%8C%E8%AF%AD%E6%B3%95%E5%88%86%E6%9E%90"><span class="nav-text">5.2 词法分析和语法分析</span></a></li><li class="nav-item nav-level-2"><a class="nav-link" href="#5-3-AST-%E8%83%BD%E5%81%9A%E4%BB%80%E4%B9%88"><span class="nav-text">5.3 AST 能做什么</span></a></li><li class="nav-item nav-level-2"><a class="nav-link" href="#5-4-AST-%E8%A7%A3%E6%9E%90%E6%B5%81%E7%A8%8B"><span class="nav-text">5.4 AST 解析流程</span></a></li><li class="nav-item nav-level-2"><a class="nav-link" href="#5-5-%E4%BF%AE%E6%94%B9%E5%87%BD%E6%95%B0%E5%90%8D%E5%AD%97"><span class="nav-text">5.5 修改函数名字</span></a></li><li class="nav-item nav-level-2"><a class="nav-link" href="#5-6-babel-%E5%B7%A5%E4%BD%9C%E5%8E%9F%E7%90%86"><span class="nav-text">5.6 babel 工作原理</span></a><ol class="nav-child"><li class="nav-item nav-level-3"><a class="nav-link" href="#5-6-1-%E6%8F%92%E4%BB%B6%E5%92%8C%E9%A2%84%E8%AE%BE%E7%9A%84%E5%8C%BA%E5%88%AB"><span class="nav-text">5.6.1 插件和预设的区别</span></a></li><li class="nav-item nav-level-3"><a class="nav-link" href="#5-6-2-babel-%E6%8F%92%E4%BB%B6%E7%9A%84%E4%BD%BF%E7%94%A8"><span class="nav-text">5.6.2 babel 插件的使用</span></a></li></ol></li><li class="nav-item nav-level-2"><a class="nav-link" href="#5-7-%E7%BC%96%E5%86%99%E8%87%AA%E5%B7%B1%E7%9A%84%E6%8F%92%E4%BB%B6"><span class="nav-text">5.7 编写自己的插件</span></a><ol class="nav-child"><li class="nav-item nav-level-3"><a class="nav-link" href="#5-7-1-%E5%88%86%E6%9E%90-AST-%E7%BB%93%E6%9E%84"><span class="nav-text">5.7.1 分析 AST 结构</span></a></li><li class="nav-item nav-level-3"><a class="nav-link" href="#5-7-2-%E8%AE%BF%E9%97%AE%E8%80%85%E6%A8%A1%E5%BC%8F"><span class="nav-text">5.7.2 访问者模式</span></a></li><li class="nav-item nav-level-3"><a class="nav-link" href="#5-7-3-%E4%BF%AE%E6%94%B9-AST-%E7%BB%93%E6%9E%84"><span class="nav-text">5.7.3 修改 AST 结构</span></a></li><li class="nav-item nav-level-3"><a class="nav-link" href="#5-7-4-%E7%89%B9%E6%AE%8A%E6%83%85%E5%86%B5"><span class="nav-text">5.7.4 特殊情况</span></a></li></ol></li><li class="nav-item nav-level-2"><a class="nav-link" href="#5-8-%E6%8C%89%E9%9C%80%E5%BC%95%E5%85%A5"><span class="nav-text">5.8 按需引入</span></a><ol class="nav-child"><li class="nav-item nav-level-3"><a class="nav-link" href="#5-8-1-%E5%88%86%E6%9E%90%E8%AF%AD%E6%B3%95%E6%A0%91"><span class="nav-text">5.8.1 分析语法树</span></a></li><li class="nav-item nav-level-3"><a class="nav-link" href="#5-8-2-%E5%88%86%E6%9E%90%E7%B1%BB%E5%9E%8B"><span class="nav-text">5.8.2 分析类型</span></a></li><li class="nav-item nav-level-3"><a class="nav-link" href="#5-8-3-%E4%B8%8A%E4%BB%A3%E7%A0%81"><span class="nav-text">5.8.3 上代码</span></a></li><li class="nav-item nav-level-3"><a class="nav-link" href="#5-8-4-%E7%89%B9%E6%AE%8A%E6%83%85%E5%86%B5"><span class="nav-text">5.8.4 特殊情况</span></a></li></ol></li><li class="nav-item nav-level-2"><a class="nav-link" href="#5-9-babylon"><span class="nav-text">5.9 babylon</span></a><ol class="nav-child"><li class="nav-item nav-level-3"><a class="nav-link" href="#5-9-1-babylon-%E4%B8%8E-babel-%E7%9A%84%E5%85%B3%E7%B3%BB"><span class="nav-text">5.9.1 babylon 与 babel 的关系</span></a></li><li class="nav-item nav-level-3"><a class="nav-link" href="#5-9-2-%E4%BD%BF%E7%94%A8-babylon"><span class="nav-text">5.9.2 使用 babylon</span></a></li><li class="nav-item nav-level-3"><a class="nav-link" href="#5-9-3-%E5%88%86%E6%9E%90%E8%AF%AD%E6%B3%95%E6%A0%91"><span class="nav-text">5.9.3 分析语法树</span></a></li><li class="nav-item nav-level-3"><a class="nav-link" href="#5-9-4-%E5%88%86%E6%9E%90%E7%B1%BB%E5%9E%8B"><span class="nav-text">5.9.4 分析类型</span></a></li><li class="nav-item nav-level-3"><a class="nav-link" href="#5-9-5-%E4%B8%8A%E4%BB%A3%E7%A0%81"><span class="nav-text">5.9.5 上代码</span></a></li></ol></li><li class="nav-item nav-level-2"><a class="nav-link" href="#5-10-%E5%85%B7%E4%BD%93%E8%AF%AD%E6%B3%95%E6%A0%91"><span class="nav-text">5.10 具体语法树</span></a></li><li class="nav-item nav-level-2"><a class="nav-link" href="#5-11-%E8%A1%A5%E5%85%85"><span class="nav-text">5.11 补充</span></a></li><li class="nav-item nav-level-2"><a class="nav-link" href="#5-12-%E9%85%8D%E5%A5%97%E6%BA%90%E7%A0%81%E5%9C%B0%E5%9D%80"><span class="nav-text">5.12 配套源码地址</span></a></li><li class="nav-item nav-level-2"><a class="nav-link" href="#%E5%8F%82%E8%80%83%E9%93%BE%E6%8E%A5"><span class="nav-text">参考链接</span></a></li></ol></li><li class="nav-item nav-level-1"><a class="nav-link" href="#6-webpack-%E6%B5%81%E7%A8%8B"><span class="nav-text">6.webpack-流程</span></a><ol class="nav-child"><li class="nav-item nav-level-2"><a class="nav-link" href="#6-1-%E5%BC%95%E8%A8%80"><span class="nav-text">6.1 引言</span></a></li><li class="nav-item nav-level-2"><a class="nav-link" href="#6-2-%E5%87%86%E5%A4%87%E5%B7%A5%E4%BD%9C"><span class="nav-text">6.2 准备工作</span></a><ol class="nav-child"><li class="nav-item nav-level-3"><a class="nav-link" href="#6-2-1-webstorm-%E4%B8%AD%E9%85%8D%E7%BD%AE-webpack-webstorm-debugger-script"><span class="nav-text">6.2.1 webstorm 中配置 webpack-webstorm-debugger-script</span></a></li><li class="nav-item nav-level-3"><a class="nav-link" href="#6-2-2-webpack-config-js-%E9%85%8D%E7%BD%AE"><span class="nav-text">6.2.2 webpack.config.js 配置</span></a></li><li class="nav-item nav-level-3"><a class="nav-link" href="#6-2-3-%E6%B5%81%E7%A8%8B%E6%80%BB%E8%A7%88"><span class="nav-text">6.2.3 流程总览</span></a></li></ol></li><li class="nav-item nav-level-2"><a class="nav-link" href="#6-3-shell-%E4%B8%8E-config-%E8%A7%A3%E6%9E%90"><span class="nav-text">6.3 shell 与 config 解析</span></a><ol class="nav-child"><li class="nav-item nav-level-3"><a class="nav-link" href="#6-3-1-optimist"><span class="nav-text">6.3.1 optimist</span></a></li><li class="nav-item nav-level-3"><a class="nav-link" href="#6-3-2-config-%E5%90%88%E5%B9%B6%E4%B8%8E%E6%8F%92%E4%BB%B6%E5%8A%A0%E8%BD%BD"><span class="nav-text">6.3.2 config 合并与插件加载</span></a></li><li class="nav-item nav-level-3"><a class="nav-link" href="#6-3-3-%E7%BC%96%E8%AF%91%E4%B8%8E%E6%9E%84%E5%BB%BA%E6%B5%81%E7%A8%8B"><span class="nav-text">6.3.3 编译与构建流程</span></a><ol class="nav-child"><li class="nav-item nav-level-4"><a class="nav-link" href="#1-%E6%A0%B8%E5%BF%83%E5%AF%B9%E8%B1%A1-Compilation"><span class="nav-text">1. 核心对象 Compilation</span></a></li><li class="nav-item nav-level-4"><a class="nav-link" href="#2-%E7%BC%96%E8%AF%91%E4%B8%8E%E6%9E%84%E5%BB%BA%E4%B8%BB%E6%B5%81%E7%A8%8B"><span class="nav-text">2. 编译与构建主流程</span></a></li><li class="nav-item nav-level-4"><a class="nav-link" href="#3-%E6%9E%84%E5%BB%BA%E7%BB%86%E8%8A%82"><span class="nav-text">3. 构建细节</span></a></li></ol></li></ol></li><li class="nav-item nav-level-2"><a class="nav-link" href="#6-4-%E6%89%93%E5%8C%85%E8%BE%93%E5%87%BA"><span class="nav-text">6.4 打包输出</span></a><ol class="nav-child"><li class="nav-item nav-level-4"><a class="nav-link" href="#1-%E7%94%9F%E6%88%90%E6%9C%80%E7%BB%88-assets"><span class="nav-text">1. 生成最终 assets</span></a></li><li class="nav-item nav-level-4"><a class="nav-link" href="#2-%E8%BE%93%E5%87%BA"><span class="nav-text">2. 输出</span></a></li></ol></li></ol></li><li class="nav-item nav-level-2"><a class="nav-link" href="#%E6%80%BB%E7%BB%93"><span class="nav-text">总结</span></a></li></ol></li><li class="nav-item nav-level-1"><a class="nav-link" href="#7-webpack-%E6%8F%92%E4%BB%B6"><span class="nav-text">7.webpack-插件</span></a><ol class="nav-child"><li class="nav-item nav-level-2"><a class="nav-link" href="#7-1-%E5%89%8D%E8%A8%80"><span class="nav-text">7.1 前言</span></a></li><li class="nav-item nav-level-2"><a class="nav-link" href="#7-2-Tapable"><span class="nav-text">7.2 Tapable</span></a><ol class="nav-child"><li class="nav-item nav-level-3"><a class="nav-link" href="#7-2-1-Tapable%E6%98%AF%E4%BB%80%E4%B9%88"><span class="nav-text">7.2.1 Tapable是什么</span></a></li><li class="nav-item nav-level-3"><a class="nav-link" href="#7-2-2-Tapable%E7%9A%84%E4%BD%BF%E7%94%A8-%EF%BC%88%E8%AF%A5%E5%B0%8F%E6%AE%B5%E5%86%85%E5%AE%B9%E5%BC%95%E7%94%A8%E6%96%87%E7%AB%A0%EF%BC%89"><span class="nav-text">7.2.2 Tapable的使用 （该小段内容引用文章）</span></a></li><li class="nav-item nav-level-3"><a class="nav-link" href="#7-2-3-Tapable%E7%9A%84%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90"><span class="nav-text">7.2.3 Tapable的源码分析</span></a></li></ol></li><li class="nav-item nav-level-2"><a class="nav-link" href="#7-3-compile"><span class="nav-text">7.3 compile</span></a><ol class="nav-child"><li class="nav-item nav-level-3"><a class="nav-link" href="#7-3-1-compile%E6%98%AF%E4%BB%80%E4%B9%88"><span class="nav-text">7.3.1 compile是什么</span></a></li><li class="nav-item nav-level-3"><a class="nav-link" href="#7-3-2-compile%E7%9A%84%E5%86%85%E9%83%A8%E5%AE%9E%E7%8E%B0"><span class="nav-text">7.3.2 compile的内部实现</span></a></li></ol></li><li class="nav-item nav-level-2"><a class="nav-link" href="#7-4-compilation"><span class="nav-text">7.4 compilation</span></a><ol class="nav-child"><li class="nav-item nav-level-3"><a class="nav-link" href="#7-4-1-%E4%BB%80%E4%B9%88%E6%98%AFcompilation"><span class="nav-text">7.4.1 什么是compilation</span></a></li><li class="nav-item nav-level-3"><a class="nav-link" href="#7-4-2-compilation%E7%9A%84%E5%AE%9E%E7%8E%B0"><span class="nav-text">7.4.2 compilation的实现</span></a></li></ol></li><li class="nav-item nav-level-2"><a class="nav-link" href="#7-5-%E7%BC%96%E5%86%99%E4%B8%80%E4%B8%AA%E6%8F%92%E4%BB%B6"><span class="nav-text">7.5 编写一个插件</span></a></li><li class="nav-item nav-level-2"><a class="nav-link" href="#7-6-compiler%E5%92%8Ccompilation%E4%B8%80%E4%BA%9B%E6%AF%94%E8%BE%83%E9%87%8D%E8%A6%81%E7%9A%84%E4%BA%8B%E4%BB%B6%E9%92%A9%E5%AD%90"><span class="nav-text">7.6 compiler和compilation一些比较重要的事件钩子</span></a><ol class="nav-child"><li class="nav-item nav-level-3"><a class="nav-link" href="#7-6-1-compier"><span class="nav-text">7.6.1 compier</span></a></li><li class="nav-item nav-level-3"><a class="nav-link" href="#7-6-2-compilation"><span class="nav-text">7.6.2 compilation</span></a></li></ol></li><li class="nav-item nav-level-2"><a class="nav-link" href="#%E6%80%BB%E7%BB%93-1"><span class="nav-text">总结</span></a></li><li class="nav-item nav-level-2"><a class="nav-link" href="#%E5%BC%95%E7%94%A8"><span class="nav-text">引用</span></a></li></ol></li><li class="nav-item nav-level-1"><a class="nav-link" href="#8-webpack-loader"><span class="nav-text">8.webpack-loader</span></a><ol class="nav-child"><li class="nav-item nav-level-2"><a class="nav-link" href="#8-1-%E9%97%AE%E9%A2%98"><span class="nav-text">8.1 问题</span></a></li><li class="nav-item nav-level-2"><a class="nav-link" href="#8-2-%E6%80%9D%E8%B7%AF"><span class="nav-text">8.2 思路</span></a></li><li class="nav-item nav-level-2"><a class="nav-link" href="#8-3-style-loader-%E7%9A%84%E5%86%8D-require"><span class="nav-text">8.3 style-loader 的再 require</span></a></li><li class="nav-item nav-level-2"><a class="nav-link" href="#8-4-loaders-%E7%9A%84%E6%8B%86%E8%A7%A3%E4%B8%8E%E8%BF%90%E8%A1%8C"><span class="nav-text">8.4 loaders 的拆解与运行</span></a></li><li class="nav-item nav-level-2"><a class="nav-link" href="#8-5-%E5%BC%82%E6%AD%A5%E7%9A%84-less-loader"><span class="nav-text">8.5 异步的 less-loader</span></a></li><li class="nav-item nav-level-2"><a class="nav-link" href="#8-6-node-modules-%E7%9A%84%E9%80%90%E7%BA%A7%E6%9F%A5%E6%89%BE"><span class="nav-text">8.6 node-modules 的逐级查找</span></a></li><li class="nav-item nav-level-2"><a class="nav-link" href="#%E5%90%8E%E8%AF%9D"><span class="nav-text">后话</span></a></li></ol></li><li class="nav-item nav-level-1"><a class="nav-link" href="#9-%E5%BE%AE%E5%89%8D%E7%AB%AF-%E5%89%8D%E7%AB%AF%E5%BE%AE%E6%9C%8D%E5%8A%A1"><span class="nav-text">9.微前端(前端微服务)</span></a><ol class="nav-child"><li class="nav-item nav-level-2"><a class="nav-link" href="#%E5%89%8D%E8%A8%80-1"><span class="nav-text">前言</span></a></li><li class="nav-item nav-level-2"><a class="nav-link" href="#9-1-%E5%BE%AE%E5%89%8D%E7%AB%AF%E7%9A%84%E4%BB%B7%E5%80%BC"><span class="nav-text">9.1 微前端的价值</span></a></li><li class="nav-item nav-level-2"><a class="nav-link" href="#9-2-%E9%92%88%E5%AF%B9%E4%B8%AD%E5%90%8E%E5%8F%B0%E5%BA%94%E7%94%A8%E7%9A%84%E8%A7%A3%E5%86%B3%E6%96%B9%E6%A1%88"><span class="nav-text">9.2 针对中后台应用的解决方案</span></a></li><li class="nav-item nav-level-2"><a class="nav-link" href="#9-3-%E8%A1%8C%E4%B8%9A%E7%8E%B0%E7%8A%B6"><span class="nav-text">9.3 行业现状</span></a></li><li class="nav-item nav-level-2"><a class="nav-link" href="#9-4-%E5%BE%AE%E5%89%8D%E7%AB%AF%E6%9E%B6%E6%9E%84%E5%AE%9E%E8%B7%B5%E4%B8%AD%E7%9A%84%E9%97%AE%E9%A2%98"><span class="nav-text">9.4 微前端架构实践中的问题</span></a></li><li class="nav-item nav-level-2"><a class="nav-link" href="#9-5-%E8%9A%82%E8%9A%81%E9%87%91%E6%9C%8D%E5%BE%AE%E5%89%8D%E7%AB%AF%E8%90%BD%E5%9C%B0%E5%AE%9E%E8%B7%B5"><span class="nav-text">9.5 蚂蚁金服微前端落地实践</span></a></li></ol></li><li class="nav-item nav-level-1"><a class="nav-link" href="#10-Nodejs%E6%A8%A1%E5%9D%97%E6%9C%BA%E5%88%B6"><span class="nav-text">10.Nodejs模块机制</span></a><ol class="nav-child"><li class="nav-item nav-level-2"><a class="nav-link" href="#10-1-CommonJS%E8%A7%84%E8%8C%83"><span class="nav-text">10.1 CommonJS规范</span></a><ol class="nav-child"><li class="nav-item nav-level-3"><a class="nav-link" href="#10-1-1-%E6%A8%A1%E5%9D%97%E5%BC%95%E7%94%A8"><span class="nav-text">10.1.1 模块引用</span></a></li><li class="nav-item nav-level-3"><a class="nav-link" href="#10-1-2-%E6%A8%A1%E5%9D%97%E5%AE%9A%E4%B9%89"><span class="nav-text">10.1.2 模块定义</span></a></li><li class="nav-item nav-level-3"><a class="nav-link" href="#10-1-3-%E6%A8%A1%E5%9D%97%E6%A0%87%E8%AF%86"><span class="nav-text">10.1.3 模块标识</span></a></li></ol></li><li class="nav-item nav-level-2"><a class="nav-link" href="#10-2-Node%E7%9A%84%E6%A8%A1%E5%9D%97%E5%AE%9E%E7%8E%B0"><span class="nav-text">10.2 Node的模块实现</span></a><ol class="nav-child"><li class="nav-item nav-level-3"><a class="nav-link" href="#10-2-1-%E8%B7%AF%E5%BE%84%E5%88%86%E6%9E%90"><span class="nav-text">10.2.1 路径分析</span></a></li><li class="nav-item nav-level-3"><a class="nav-link" href="#10-2-2-%E6%96%87%E4%BB%B6%E5%AE%9A%E4%BD%8D"><span class="nav-text">10.2.2 文件定位</span></a></li><li class="nav-item nav-level-3"><a class="nav-link" href="#10-2-3-%E6%A8%A1%E5%9D%97%E7%BC%96%E8%AF%91"><span class="nav-text">10.2.3 模块编译</span></a></li></ol></li><li class="nav-item nav-level-2"><a class="nav-link" href="#10-3-import%E5%92%8Crequire"><span class="nav-text">10.3 import和require</span></a></li><li class="nav-item nav-level-2"><a class="nav-link" href="#10-4-nodejs%E6%B8%85%E9%99%A4require%E7%BC%93%E5%AD%98"><span class="nav-text">10.4 nodejs清除require缓存</span></a></li></ol></li><li class="nav-item nav-level-1"><a class="nav-link" href="#11-require%E5%8E%9F%E7%90%86"><span class="nav-text">11.require原理</span></a><ol class="nav-child"><li class="nav-item nav-level-2"><a class="nav-link" href="#11-1-require-%E7%9A%84%E5%9F%BA%E6%9C%AC%E7%94%A8%E6%B3%95"><span class="nav-text">11.1 require() 的基本用法</span></a></li><li class="nav-item nav-level-2"><a class="nav-link" href="#11-2-Module-%E6%9E%84%E9%80%A0%E5%87%BD%E6%95%B0"><span class="nav-text">11.2 Module 构造函数</span></a></li><li class="nav-item nav-level-2"><a class="nav-link" href="#11-3-%E6%A8%A1%E5%9D%97%E5%AE%9E%E4%BE%8B%E7%9A%84-require-%E6%96%B9%E6%B3%95"><span class="nav-text">11.3 模块实例的 require 方法</span></a></li><li class="nav-item nav-level-2"><a class="nav-link" href="#11-4-%E6%A8%A1%E5%9D%97%E7%9A%84%E7%BB%9D%E5%AF%B9%E8%B7%AF%E5%BE%84"><span class="nav-text">11.4 模块的绝对路径</span></a></li><li class="nav-item nav-level-2"><a class="nav-link" href="#11-5-%E5%8A%A0%E8%BD%BD%E6%A8%A1%E5%9D%97"><span class="nav-text">11.5 加载模块</span></a></li></ol></li><li class="nav-item nav-level-1"><a class="nav-link" href="#12-Node-js-%E4%BA%8B%E4%BB%B6%E5%BE%AA%E7%8E%AF"><span class="nav-text">12.Node.js 事件循环</span></a><ol class="nav-child"><li class="nav-item nav-level-2"><a class="nav-link" href="#%E5%89%8D%E8%A8%80-Node-%E4%BA%8B%E4%BB%B6%E5%BE%AA%E7%8E%AF"><span class="nav-text">前言 Node 事件循环</span></a></li><li class="nav-item nav-level-2"><a class="nav-link" href="#12-1-%E4%BB%80%E4%B9%88%E6%98%AF%E4%BA%8B%E4%BB%B6%E5%BE%AA%E7%8E%AF-What-is-the-Event-Loop"><span class="nav-text">12.1 什么是事件循环 (What is the Event Loop)?</span></a></li><li class="nav-item nav-level-2"><a class="nav-link" href="#12-2-%E8%BF%99%E5%B0%B1%E6%98%AF%E4%BA%8B%E4%BB%B6%E5%BE%AA%E7%8E%AF-Event-Loop-Explained"><span class="nav-text">12.2 这就是事件循环 (Event Loop Explained)</span></a></li><li class="nav-item nav-level-2"><a class="nav-link" href="#12-3-%E5%90%84%E9%98%B6%E6%AE%B5%E6%A6%82%E8%A7%88-Phases-Overview"><span class="nav-text">12.3 各阶段概览 Phases Overview</span></a></li><li class="nav-item nav-level-2"><a class="nav-link" href="#12-4-%E5%90%84%E9%98%B6%E6%AE%B5%E8%AF%A6%E7%BB%86%E8%A7%A3%E9%87%8A-Phases-in-Detail"><span class="nav-text">12.4 各阶段详细解释 Phases in Detail</span></a><ol class="nav-child"><li class="nav-item nav-level-3"><a class="nav-link" href="#12-4-1-timers-%E8%AE%A1%E6%97%B6%E5%99%A8%E9%98%B6%E6%AE%B5"><span class="nav-text">12.4.1 timers 计时器阶段</span></a></li><li class="nav-item nav-level-3"><a class="nav-link" href="#12-4-2-pending-callbacks-%E9%98%B6%E6%AE%B5"><span class="nav-text">12.4.2 pending callbacks 阶段</span></a></li><li class="nav-item nav-level-3"><a class="nav-link" href="#12-4-3-%E8%BD%AE%E8%AF%A2-poll-%E9%98%B6%E6%AE%B5"><span class="nav-text">12.4.3 轮询 poll 阶段</span></a></li><li class="nav-item nav-level-3"><a class="nav-link" href="#12-4-4-%E6%A3%80%E6%9F%A5%E9%98%B6%E6%AE%B5-check"><span class="nav-text">12.4.4 检查阶段 check</span></a></li><li class="nav-item nav-level-3"><a class="nav-link" href="#12-4-5-close-callbacks-%E9%98%B6%E6%AE%B5"><span class="nav-text">12.4.5 close callbacks 阶段</span></a></li></ol></li><li class="nav-item nav-level-2"><a class="nav-link" href="#12-5-setImmediate-vs-setTimeout"><span class="nav-text">12.5 setImmediate vs setTimeout</span></a></li><li class="nav-item nav-level-2"><a class="nav-link" href="#12-6process-nextTick"><span class="nav-text">12.6process.nextTick</span></a><ol class="nav-child"><li class="nav-item nav-level-3"><a class="nav-link" href="#12-6-1-%E7%90%86%E8%A7%A3-process-nextTick"><span class="nav-text">12.6.1 理解 process.nextTick</span></a></li><li class="nav-item nav-level-3"><a class="nav-link" href="#12-6-2-%E4%B8%BA%E4%BB%80%E4%B9%88%E5%85%81%E8%AE%B8%E8%BF%99%E6%A0%B7%E6%93%8D%E4%BD%9C%EF%BC%9F-Why-would-that-be-allowed"><span class="nav-text">12.6.2 为什么允许这样操作？ Why would that be allowed?</span></a></li><li class="nav-item nav-level-3"><a class="nav-link" href="#12-6-3-process-nextTick-vs-setImmediate"><span class="nav-text">12.6.3 process.nextTick vs setImmediate</span></a></li><li class="nav-item nav-level-3"><a class="nav-link" href="#12-6-4-%E4%B8%BA%E4%BB%80%E4%B9%88%E8%BF%98%E7%94%A8-process-nextTick%EF%BC%9F"><span class="nav-text">12.6.4 为什么还用 process.nextTick？</span></a></li><li class="nav-item nav-level-3"><a class="nav-link" href="#12-6-5-process-nextTick-%E5%9C%A8%E4%BA%8B%E4%BB%B6%E5%BE%AA%E7%8E%AF%E7%9A%84%E4%BD%8D%E7%BD%AE%EF%BC%9A"><span class="nav-text">12.6.5 process.nextTick 在事件循环的位置：</span></a></li></ol></li><li class="nav-item nav-level-2"><a class="nav-link" href="#12-7-Microtasks-%E5%BE%AE%E4%BB%BB%E5%8A%A1"><span class="nav-text">12.7 Microtasks 微任务</span></a></li><li class="nav-item nav-level-2"><a class="nav-link" href="#12-8%E9%A2%98%E5%A4%96%E8%AF%9D%EF%BC%9AEvents"><span class="nav-text">12.8题外话：Events</span></a></li></ol></li><li class="nav-item nav-level-1"><a class="nav-link" href="#13-cluster%E5%8E%9F%E7%90%86"><span class="nav-text">13.cluster原理</span></a><ol class="nav-child"><li class="nav-item nav-level-2"><a class="nav-link" href="#13-1-%E6%A6%82%E8%BF%B0"><span class="nav-text">13.1 概述</span></a></li><li class="nav-item nav-level-2"><a class="nav-link" href="#13-2-%E7%BA%BF%E7%A8%8B%E4%B8%8E%E8%BF%9B%E7%A8%8B"><span class="nav-text">13.2 线程与进程</span></a></li><li class="nav-item nav-level-2"><a class="nav-link" href="#13-3-cluster%E6%A8%A1%E5%9D%97%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90"><span class="nav-text">13.3 cluster模块源码解析</span></a><ol class="nav-child"><li class="nav-item nav-level-3"><a class="nav-link" href="#13-3-1-%E8%B5%B7%E6%AD%A5"><span class="nav-text">13.3.1 起步</span></a></li><li class="nav-item nav-level-3"><a class="nav-link" href="#13-3-2-%E5%85%A5%E5%8F%A3"><span class="nav-text">13.3.2 入口</span></a></li><li class="nav-item nav-level-3"><a class="nav-link" href="#13-3-3-%E4%B8%BB%E8%BF%9B%E7%A8%8B%E6%A8%A1%E5%9D%97master-js"><span class="nav-text">13.3.3 主进程模块master.js</span></a></li><li class="nav-item nav-level-3"><a class="nav-link" href="#13-3-4-%E5%AD%90%E8%BF%9B%E7%A8%8B%E6%A8%A1%E5%9D%97child-js"><span class="nav-text">13.3.4 子进程模块child.js</span></a></li></ol></li><li class="nav-item nav-level-2"><a class="nav-link" href="#13-4-%E5%B0%8F%E7%BB%93"><span class="nav-text">13.4 小结</span></a></li></ol></li><li class="nav-item nav-level-1"><a class="nav-link" href="#14-%E6%B5%81%E6%9C%BA%E5%88%B6"><span class="nav-text">14.流机制</span></a><ol class="nav-child"><li class="nav-item nav-level-2"><a class="nav-link" href="#14-1-stream-%E6%A8%A1%E5%9D%97"><span class="nav-text">14.1 stream 模块</span></a></li><li class="nav-item nav-level-2"><a class="nav-link" href="#14-2-Readable-Stream"><span class="nav-text">14.2 Readable Stream</span></a></li><li class="nav-item nav-level-2"><a class="nav-link" href="#14-3-Writable-Stream"><span class="nav-text">14.3 Writable Stream</span></a></li><li class="nav-item nav-level-2"><a class="nav-link" href="#14-4-pipe"><span class="nav-text">14.4 pipe</span></a></li><li class="nav-item nav-level-2"><a class="nav-link" href="#14-5-Duplex-Stream"><span class="nav-text">14.5 Duplex Stream</span></a></li><li class="nav-item nav-level-2"><a class="nav-link" href="#14-6-Transform-Stream"><span class="nav-text">14.6 Transform Stream</span></a></li><li class="nav-item nav-level-2"><a class="nav-link" href="#%E5%B0%8F%E7%BB%93"><span class="nav-text">小结</span></a></li></ol></li><li class="nav-item nav-level-1"><a class="nav-link" href="#15-pipe%E5%8E%9F%E7%90%86"><span class="nav-text">15.pipe原理</span></a><ol class="nav-child"><li class="nav-item nav-level-2"><a class="nav-link" href="#15-1-Nodejs-Stream-pipe-%E5%9F%BA%E6%9C%AC%E7%A4%BA%E4%BE%8B"><span class="nav-text">15.1 Nodejs Stream pipe 基本示例</span></a><ol class="nav-child"><li class="nav-item nav-level-3"><a class="nav-link" href="#15-1-1-%E6%9C%AA%E4%BD%BF%E7%94%A8-Stream-pipe-%E6%83%85%E5%86%B5"><span class="nav-text">15.1.1 未使用 Stream pipe 情况</span></a></li><li class="nav-item nav-level-3"><a class="nav-link" href="#15-1-2-%E4%BD%BF%E7%94%A8-Stream-pipe-%E6%83%85%E5%86%B5"><span class="nav-text">15.1.2 使用 Stream pipe 情况</span></a></li><li class="nav-item nav-level-3"><a class="nav-link" href="#15-1-3-%E4%BD%BF%E7%94%A8-Stream-VS-%E4%B8%8D%E4%BD%BF%E7%94%A8-Stream"><span class="nav-text">15.1.3 使用 Stream VS 不使用 Stream</span></a></li></ol></li><li class="nav-item nav-level-2"><a class="nav-link" href="#15-2-pipe-%E7%9A%84%E8%B0%83%E7%94%A8%E8%BF%87%E7%A8%8B%E4%B8%8E%E5%AE%9E%E7%8E%B0%E5%8E%9F%E7%90%86%E5%88%86%E6%9E%90"><span class="nav-text">15.2 pipe 的调用过程与实现原理分析</span></a><ol class="nav-child"><li class="nav-item nav-level-3"><a class="nav-link" href="#15-2-1-%E9%A1%BA%E8%97%A4%E6%91%B8%E7%93%9C"><span class="nav-text">15.2.1 顺藤摸瓜</span></a><ol class="nav-child"><li class="nav-item nav-level-4"><a class="nav-link" href="#15-2-1-1-lib-fs-js"><span class="nav-text">15.2.1.1 &#x2F;lib&#x2F;fs.js</span></a></li><li class="nav-item nav-level-4"><a class="nav-link" href="#15-2-1-2-lib-internal-fs-streams-js"><span class="nav-text">15.2.1.2 &#x2F;lib&#x2F;internal&#x2F;fs&#x2F;streams.js</span></a></li><li class="nav-item nav-level-4"><a class="nav-link" href="#15-2-1-3-lib-stream-js"><span class="nav-text">15.2.1.3 &#x2F;lib&#x2F;stream.js</span></a></li><li class="nav-item nav-level-4"><a class="nav-link" href="#15-2-1-4-lib-internal-streams-legacy-js"><span class="nav-text">15.2.1.4 &#x2F;lib&#x2F;internal&#x2F;streams&#x2F;legacy.js</span></a></li><li class="nav-item nav-level-4"><a class="nav-link" href="#15-2-1-5-lib-stream-readable-js"><span class="nav-text">15.2.1.5 &#x2F;lib&#x2F;_stream_readable.js</span></a></li></ol></li><li class="nav-item nav-level-3"><a class="nav-link" href="#15-2-2-stream-readable-%E5%AE%9E%E7%8E%B0%E5%88%86%E6%9E%90"><span class="nav-text">15.2.2 _stream_readable 实现分析</span></a><ol class="nav-child"><li class="nav-item nav-level-4"><a class="nav-link" href="#15-2-2-1-%E5%A3%B0%E6%98%8E%E6%9E%84%E9%80%A0%E5%87%BD%E6%95%B0-Readable"><span class="nav-text">15.2.2.1 声明构造函数 Readable</span></a></li><li class="nav-item nav-level-4"><a class="nav-link" href="#15-2-2-2-%E5%A3%B0%E6%98%8E-pipe-%E6%96%B9%E6%B3%95%EF%BC%8C%E8%AE%A2%E9%98%85-data-%E4%BA%8B%E4%BB%B6"><span class="nav-text">15.2.2.2 声明 pipe 方法，订阅 data 事件</span></a></li><li class="nav-item nav-level-4"><a class="nav-link" href="#15-2-2-3-%E8%AE%A2%E9%98%85-drain-%E4%BA%8B%E4%BB%B6%EF%BC%8C%E7%BB%A7%E7%BB%AD%E6%B5%81%E5%8A%A8%E6%95%B0%E6%8D%AE"><span class="nav-text">15.2.2.3 订阅 drain 事件，继续流动数据</span></a></li><li class="nav-item nav-level-4"><a class="nav-link" href="#15-2-2-4-%E8%A7%A6%E5%8F%91-data-%E4%BA%8B%E4%BB%B6"><span class="nav-text">15.2.2.4 触发 data 事件</span></a></li><li class="nav-item nav-level-4"><a class="nav-link" href="#15-2-2-5-%E8%AE%A2%E9%98%85-end-%E4%BA%8B%E4%BB%B6"><span class="nav-text">15.2.2.5 订阅 end 事件</span></a></li><li class="nav-item nav-level-4"><a class="nav-link" href="#15-2-2-6-%E8%A7%A6%E5%8F%91-pipe-%E4%BA%8B%E4%BB%B6"><span class="nav-text">15.2.2.6 触发 pipe 事件</span></a></li><li class="nav-item nav-level-4"><a class="nav-link" href="#15-2-2-7-%E6%94%AF%E6%8C%81%E9%93%BE%E5%BC%8F%E8%B0%83%E7%94%A8"><span class="nav-text">15.2.2.7 支持链式调用</span></a></li></ol></li></ol></li><li class="nav-item nav-level-2"><a class="nav-link" href="#15-3-%E6%80%BB%E7%BB%93"><span class="nav-text">15.3 总结</span></a></li><li class="nav-item nav-level-2"><a class="nav-link" href="#15-4-Reference"><span class="nav-text">15.4 Reference</span></a></li></ol></li><li class="nav-item nav-level-1"><a class="nav-link" href="#16-%E5%AE%88%E6%8A%A4%E8%BF%9B%E7%A8%8B"><span class="nav-text">16.守护进程</span></a><ol class="nav-child"><li class="nav-item nav-level-2"><a class="nav-link" href="#16-1-Nodejs%E7%BC%96%E5%86%99%E5%AE%88%E6%8A%A4%E8%BF%9B%E7%A8%8B"><span class="nav-text">16.1 Nodejs编写守护进程</span></a></li><li class="nav-item nav-level-2"><a class="nav-link" href="#16-2-%E5%AE%88%E6%8A%A4%E8%BF%9B%E7%A8%8B%E7%9A%84%E5%90%AF%E5%8A%A8%E6%96%B9%E5%BC%8F"><span class="nav-text">16.2 守护进程的启动方式</span></a></li><li class="nav-item nav-level-2"><a class="nav-link" href="#16-3-setsid%E8%AF%A6%E8%A7%A3"><span class="nav-text">16.3 setsid详解</span></a></li><li class="nav-item nav-level-2"><a class="nav-link" href="#16-4-Nodejs%E4%B8%AD%E5%90%AF%E5%8A%A8%E5%AD%90%E8%BF%9B%E7%A8%8B%E6%96%B9%E6%B3%95"><span class="nav-text">16.4 Nodejs中启动子进程方法</span></a></li><li class="nav-item nav-level-2"><a class="nav-link" href="#16-5-Nodejs%E4%B8%ADsetsid%E7%9A%84%E8%B0%83%E7%94%A8"><span class="nav-text">16.5 Nodejs中setsid的调用</span></a></li><li class="nav-item nav-level-2"><a class="nav-link" href="#%E6%80%BB%E7%BB%93-2"><span class="nav-text">总结</span></a></li></ol></li><li class="nav-item nav-level-1"><a class="nav-link" href="#17-Nodejs%E8%BF%9B%E7%A8%8B%E9%97%B4%E9%80%9A%E4%BF%A1"><span class="nav-text">17.Nodejs进程间通信</span></a><ol class="nav-child"><li class="nav-item nav-level-2"><a class="nav-link" href="#17-1-%E5%9C%BA%E6%99%AF"><span class="nav-text">17.1 场景</span></a></li><li class="nav-item nav-level-2"><a class="nav-link" href="#17-2-%E5%88%9B%E5%BB%BA%E8%BF%9B%E7%A8%8B"><span class="nav-text">17.2 创建进程</span></a><ol class="nav-child"><li class="nav-item nav-level-3"><a class="nav-link" href="#spawn"><span class="nav-text">spawn</span></a><ol class="nav-child"><li class="nav-item nav-level-4"><a class="nav-link" href="#IPC%E9%80%89%E9%A1%B9"><span class="nav-text">IPC选项</span></a></li></ol></li><li class="nav-item nav-level-3"><a class="nav-link" href="#exec"><span class="nav-text">exec</span></a></li><li class="nav-item nav-level-3"><a class="nav-link" href="#execFile"><span class="nav-text">execFile</span></a><ol class="nav-child"><li class="nav-item nav-level-4"><a class="nav-link" href="#xxxSync"><span class="nav-text">xxxSync</span></a></li></ol></li><li class="nav-item nav-level-3"><a class="nav-link" href="#fork"><span class="nav-text">fork</span></a></li></ol></li><li class="nav-item nav-level-2"><a class="nav-link" href="#17-3-%E9%80%9A%E4%BF%A1%E6%96%B9%E5%BC%8F"><span class="nav-text">17.3 通信方式</span></a><ol class="nav-child"><li class="nav-item nav-level-3"><a class="nav-link" href="#17-3-1-%E9%80%9A%E8%BF%87stdin-stdout%E4%BC%A0%E9%80%92json"><span class="nav-text">17.3.1 通过stdin&#x2F;stdout传递json</span></a></li><li class="nav-item nav-level-3"><a class="nav-link" href="#17-3-2-%E5%8E%9F%E7%94%9FIPC%E6%94%AF%E6%8C%81"><span class="nav-text">17.3.2 原生IPC支持</span></a></li><li class="nav-item nav-level-3"><a class="nav-link" href="#17-3-3-sockets"><span class="nav-text">17.3.3 sockets</span></a></li><li class="nav-item nav-level-3"><a class="nav-link" href="#17-3-4-message-queue"><span class="nav-text">17.3.4 message queue</span></a></li><li class="nav-item nav-level-3"><a class="nav-link" href="#17-3-5-Redis"><span class="nav-text">17.3.5 Redis</span></a></li></ol></li><li class="nav-item nav-level-2"><a class="nav-link" href="#17-4-%E6%80%BB%E7%BB%93"><span class="nav-text">17.4 总结</span></a></li><li class="nav-item nav-level-2"><a class="nav-link" href="#%E5%8F%82%E8%80%83%E8%B5%84%E6%96%99"><span class="nav-text">参考资料</span></a></li></ol></li><li class="nav-item nav-level-1"><a class="nav-link" href="#18-Node-js-%E6%8D%95%E8%8E%B7%E5%BC%82%E5%B8%B8"><span class="nav-text">18.Node.js 捕获异常</span></a><ol class="nav-child"><li class="nav-item nav-level-2"><a class="nav-link" href="#18-1-try-catch-%E4%B9%8B%E7%97%9B"><span class="nav-text">18.1 try&#x2F;catch 之痛</span></a></li><li class="nav-item nav-level-2"><a class="nav-link" href="#18-2-%E4%BD%BF%E7%94%A8-domain-%E6%A8%A1%E5%9D%97%E6%8D%95%E6%8D%89%E5%BC%82%E5%B8%B8"><span class="nav-text">18.2 使用 domain 模块捕捉异常</span></a><ol class="nav-child"><li class="nav-item nav-level-3"><a class="nav-link" href="#18-2-1-domain-%E7%9A%84%E9%9A%90%E5%BC%8F%E7%BB%91%E5%AE%9A"><span class="nav-text">18.2.1 domain 的隐式绑定</span></a></li><li class="nav-item nav-level-3"><a class="nav-link" href="#18-2-2-domain-%E7%9A%84%E6%98%BE%E5%BC%8F%E7%BB%91%E5%AE%9A"><span class="nav-text">18.2.2 domain 的显式绑定</span></a></li><li class="nav-item nav-level-3"><a class="nav-link" href="#18-2-3-domain-%E5%A6%82%E4%BD%95%E6%8A%9B%E5%87%BA%E5%BC%82%E5%B8%B8"><span class="nav-text">18.2.3 domain 如何抛出异常</span></a><ol class="nav-child"><li class="nav-item nav-level-4"><a class="nav-link" href="#%E6%9B%B4%E5%A5%BD%E7%9A%84%E4%BD%BF%E7%94%A8-domain"><span class="nav-text">更好的使用 domain</span></a></li><li class="nav-item nav-level-4"><a class="nav-link" href="#domain-%E7%9A%84%E7%BC%BA%E9%99%B7"><span class="nav-text">domain 的缺陷</span></a></li><li class="nav-item nav-level-4"><a class="nav-link" href="#domain-%E5%AE%9E%E8%B7%B5"><span class="nav-text">domain 实践</span></a></li></ol></li></ol></li></ol></li></ol></div>
      </div>
      <!--/noindex-->

      <div class="site-overview-wrap sidebar-panel">
        <div class="site-author motion-element" itemprop="author" itemscope itemtype="http://schema.org/Person">
    <img class="site-author-image" itemprop="image" alt="hxy"
      src="/images/Robben.gif">
  <p class="site-author-name" itemprop="name">hxy</p>
  <div class="site-description" itemprop="description"></div>
</div>
<div class="site-state-wrap motion-element">
  <nav class="site-state">
      <div class="site-state-item site-state-posts">
          <a href="/archives/">
        
          <span class="site-state-item-count">80</span>
          <span class="site-state-item-name">日志</span>
        </a>
      </div>
      <div class="site-state-item site-state-categories">
            <a href="/categories/">
          
        <span class="site-state-item-count">8</span>
        <span class="site-state-item-name">分类</span></a>
      </div>
      <div class="site-state-item site-state-tags">
            <a href="/tags/">
          
        <span class="site-state-item-count">120</span>
        <span class="site-state-item-name">标签</span></a>
      </div>
  </nav>
</div>
  <div class="links-of-author motion-element">
      <span class="links-of-author-item">
        <a href="https://github.com/huxingyi1997" title="GitHub → https:&#x2F;&#x2F;github.com&#x2F;huxingyi1997" rel="noopener" target="_blank"><i class="fab fa-github fa-fw"></i>GitHub</a>
      </span>
      <span class="links-of-author-item">
        <a href="mailto:huxingyi1997@zju.edu.cn" title="E-Mail → mailto:huxingyi1997@zju.edu.cn" rel="noopener" target="_blank"><i class="fa fa-envelope fa-fw"></i>E-Mail</a>
      </span>
  </div>



      </div>

    </div>
  </aside>
  <div id="sidebar-dimmer"></div>


      </div>
    </main>

    <footer class="footer">
      <div class="footer-inner">
        

        

<div class="copyright">
  
  &copy; 
  <span itemprop="copyrightYear">2022</span>
  <span class="with-love">
    <i class="fa fa-frog"></i>
  </span>
  <span class="author" itemprop="copyrightHolder">hxy</span>
</div>

<div class="theme-info">
  <div class="powered-by"></div>
  <span class="post-count">博客全站共1039.2k字</span>
</div>

        
<div class="busuanzi-count">
  <script async src="https://busuanzi.ibruce.info/busuanzi/2.3/busuanzi.pure.mini.js"></script>
    <span class="post-meta-item" id="busuanzi_container_site_uv" style="display: none;">
      <span class="post-meta-item-icon">
        <i class="fa fa-user"></i>
      </span>
      <span class="site-uv" title="总访客量">
        <span id="busuanzi_value_site_uv"></span>
      </span>
    </span>
    <span class="post-meta-divider">|</span>
    <span class="post-meta-item" id="busuanzi_container_site_pv" style="display: none;">
      <span class="post-meta-item-icon">
        <i class="fa fa-eye"></i>
      </span>
      <span class="site-pv" title="总访问量">
        <span id="busuanzi_value_site_pv"></span>
      </span>
    </span>
</div>








      </div>
    </footer>
  </div>

  
  <script src="/lib/anime.min.js"></script>
  <script src="//cdn.jsdelivr.net/npm/lozad@1/dist/lozad.min.js"></script>
  <script src="/lib/velocity/velocity.min.js"></script>
  <script src="/lib/velocity/velocity.ui.min.js"></script>

<script src="/js/utils.js"></script>

<script src="/js/motion.js"></script>


<script src="/js/schemes/pisces.js"></script>


<script src="/js/next-boot.js"></script>




  




  
<script src="/js/local-search.js"></script>













  

  


<script>
NexT.utils.loadComments(document.querySelector('#valine-comments'), () => {
  NexT.utils.getScript('//unpkg.com/valine/dist/Valine.min.js', () => {
    var GUEST = ['nick', 'mail', 'link'];
    var guest = 'nick,mail,link';
    guest = guest.split(',').filter(item => {
      return GUEST.includes(item);
    });
    new Valine({
      el         : '#valine-comments',
      verify     : false,
      notify     : true,
      appId      : 'pQsO3ySbU4VtWN2j1FLA74Ha-gzGzoHsz',
      appKey     : 'QYacMDY2VY7Wazprg1X6FiUv',
      placeholder: "Just go go",
      avatar     : 'mm',
      meta       : guest,
      pageSize   : '10' || 10,
      visitor    : false,
      lang       : 'zh-cn' || 'zh-cn',
      path       : location.pathname,
      recordIP   : false,
      serverURLs : ''
    });
  }, window.Valine);
});
</script>

  
  <!-- 动态背景特效 -->
  <!-- 樱花特效 -->
    <script async src="/js/src/sakura.js"></script>
    <script async src="/js/src/fairyDustCursor.js"></script>
</body>
</html>
